islishude / blog

my web notes
https://islishude.github.io/blog/
101 stars 15 forks source link

Go: 正确处理 net 包中的错误 #234

Closed islishude closed 4 years ago

islishude commented 4 years ago

Go 网络开发中的常用的应用层协议库都是基于 net 标准库开发的,比如 gorilla/websocket 和 go-grpc,还有最常用的 http 库。

当请求一个不存在的 host 时会返回一个错误,如下所示

$ cat main.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    if _, err := http.Get("http://unknownhost.example"); err != nil {
        fmt.Println(err)
    }
}
$ go run main.go
Get "http://unknownhost.example": dial tcp: lookup unknownhost.example: no such host

这个错误本质其实是 *net.DNSError,在 Go 早期开发中,会经常使用接口断言匹配错误。

比如这样 if v, ok := err.(*net.DNSError);ok {} 的形式。

如果这样使用的话,实际上不会匹配断言匹配到,在 debug 模式可以看到最后返回其实是 *url.Error,然后内部才实际包裹了 (*net.DNSError) 错误,并实现 Unwarp() error 方法。

// url.Error
type Error struct {
    Op  string
    URL string
    Err error // 上述例子中,这里包裹了 *net.DNSError 错误
}

在 go 1.13 之后,errors.As 可以进行解包错误,那么我们就可以直接使用下面方式即可:

package main

import (
    "errors"
    "fmt"
    "net"
    "net/http"
)

func main() {
    if _, err := http.Get("http://unknownhost.example"); err != nil {
        if v := (*net.DNSError)(nil); errors.As(err, &v) {
            fmt.Println("can't found", v.Name)
        }
    }
}

除了 *net.DNSError 之外,还有下面内置错误结构体:

上面错误除了实现了 Error() 接口,还实现了 Timeout() boolTemporary() bool 接口,也就是实现了 net.Error 接口

// net.Error
type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

同时也实现了 Unwarp() 接口,用于错误解包。那么我们就可以区分请求的错误是网络错误,还是服务器错误。

比如在 jsonrpc 协议中定义了 Error 结构体,使用下面方式进行判断

type JsonError struct {
    Code int         `json:"code"`
    Msg  string      `json:"message"`
    Data interface{} `json:"data"`
}

func (e *JsonError) Error() string {
    return e.Msg
}

func main() {
    if _, err := jsonrpc.Call("http://jsonrpc.example"); err != nil {
        if v := (net.Error)(nil); errors.As(err, &v) {
            fmt.Println("is timeout?", v.Timeout())
            return
        }

        if v := (*Error)(nil); errors.As(err, &v) {
            fmt.Println("jsonrpc error: Code:", v.Code, "Msg", v.Msg)
        }
    }
}

如果按照以前的方式不断通过接口断言的解包错误的形式,那么代码很快会形成类似”回调地狱“的模样。

所以,在实际应用层开发上,直接可以使用 fmt.Errorf("Stack: %w",err) 包装错误,最后调用方最后使用 errros.Aserrros.Is 解包按需处理错误。