Go 中如何准确地判断和识别各种网络错误

Go 自带的网络标准库可能让很多第一次使用它的人感慨,这个库让网络编程的门槛低到了令人发指的地步。然而,封装层次与开发人员的可控性往往是矛盾的。Go 的网络库封装程度算是一个不错的折衷,绝大部分时候,我们只需要调用 Dial, Read, Write Close 几个基本操作就可以了。

但是,网络是复杂的。我们有时候需要细致的处理网络中的各种错误,根据不同的错误进行不同的处理。比如我们遇到一个网络错误时,需要区分这个错误是因为无法解析 host ip, 还是 TCP 无法建立连接,亦或是读写超时。一开始的时候,我们的写法可能是这样的:

    errString := err.Error()
    fmt.Println(errString)
    switch {
    case strings.Contains(errString, "timeout"):
        fmt.Println("Timeout")
    case strings.Contains(errString, "no such host"):
        fmt.Println("Unknown host")
    case strings.Contains(errString, "connection refused"):
        fmt.Println("Connection refused")
    default:
        fmt.Printf("Unknown error:%s", errString)
    }

这种根据错误信息进行字符串匹配进行判断的方法有非常明显的局限性:该错误信息依赖于操作系统,不同的操作系统对于同一错误返回的字符串信息可能是不同的。因此,这种判断网络错误类型的方法是不可靠的。那么有没有一种准确而可靠的判断各种网络错误的方式呢?答案是肯定的。

我们知道在 Go 中,error 是一个内建的 interface 类型:

type error interface {
        Error() string
}

要准确判断不同的错误类型,我们只需要类型断言出其错误类型即可。

在 Go 的网络标准库中,错误类型被统一封装为 net.Errorinterface 类型:

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

net.Error 类型的具体 concrete 类型又被封装为 net.OpError 类型:

type OpError struct {
        // Op is the operation which caused the error, such as
        // "dial", "read" or "write".
        Op string

        // Net is the network type on which this error occurred,
        // such as "tcp" or "udp6".
        Net string

        // For operations involving a remote network connection, like
        // Dial, Read, or Write, Source is the corresponding local
        // network address.
        Source Addr

        // Addr is the network address for which this error occurred.
        // For local operations, like Listen or SetDeadline, Addr is
        // the address of the local endpoint being manipulated.
        // For operations involving a remote network connection, like
        // Dial, Read, or Write, Addr is the remote address of that
        // connection.
        Addr Addr

        // Err is the error that occurred during the operation.
        Err error
}

其中,net.OpError.Err 可能是以下几种类型:

*os.SyscallError 错误比较特殊,与具体操作系统调用有关:

type SyscallError struct {
        Syscall string
        Err     error
}

对于我们关心的网络错误,SyscallError.Err 一般为 sys.Errno 类型,与网络错误相关的常用值有:

  • syscall.ECONNREFUSED
  • syscall.ETIMEDOUT

看到这里,你可能忍不住要吐槽 Go 这种错误嵌套处理了,事实上,官方也意识到了这种错误处理的问题,在 Go 2中,可能会出现新的错误和异常处理方式,可以参见 GopherChina 2018 keynote 点评: RETHINKING ERRORS FOR GO 2.

当前阶段,我们依然要直面这种错误处理方式。为了方便大家理解 Go 网络标准库中处理错误的方式,我们把上面的错误嵌套整理了一张关系图:

明白了网络标准库中处理错误的逻辑,判断和识别各种类型的网络错误就非常简单了:对网络错误进行类型断言。以我们团队主要关心的 DNS 解析错误、TCP 无法建立连接、读写超时为例,判断逻辑可以是这样:

func isCaredNetError(err error) bool {
    netErr, ok := err.(net.Error)
    if !ok {
        return false
    }

    if netErr.Timeout() {
        log.Println("timeout")
        return true
    }

    opErr, ok := netErr.(*net.OpError)
    if !ok {
        return false
    }

    switch t := opErr.Err.(type) {
    case *net.DNSError:
        log.Printf("net.DNSError:%+v", t)
        return true
    case *os.SyscallError:
        log.Printf("os.SyscallError:%+v", t)
        if errno, ok := t.Err.(syscall.Errno); ok {
            switch errno {
            case syscall.ECONNREFUSED:
                log.Println("connect refused")
                return true
            case syscall.ETIMEDOUT:
                log.Println("timeout")
                return true
            }
        }
    }

    return false
}

这种错误判定方式除了能解决最开始提到的可靠性和准确性问题,也具有良好的普适性。即基于 net 的其他标准库,如 net/http 也支持这种错误判断方式。

扩展阅读