Go 1.13 之后的 error 检查

Go 小记 2020-05-24 9619 字 993 浏览 点赞

起步

如果说 Go 有很多诟病的地方,那么 Go 中 error 的处理一定可以挤进吐槽榜单前十。既然 try语句提议被一拒再拒,我们也只好用着古老的 if 筛选错误。Go 官方并非没有意识到 error 的鸡肋问题,于是在 Go 1.13 提出了新解决方案,总的说来就是“三个 api + 一个格式占位符”。

error 从何而来

在 Go 中,error 从何而来呢?熟练使用 Go 的人一定知道以下几种方式:

  • errors.New
  • fmt.Errorf
  • 直接返回函数调用后得到的 error
  • 定义一个结构体,实现 error 接口(type error Interface { Error() string }

以“打开一个文件”为例,按照上面的处理方式代码可以分别是:

// errors.New
func openConfigFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        // 返回新的 error 实例
        return errors.New("can not open the specific file")
    }
    return nil
}
// fmt.Errorf
func openConfigFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        // 返回新的 error 实例,同时包含了实际 error
        return fmt.Errorf("can not open the specific file, reason: %v", err)
    }
    return nil
}
// return error that called function returned
func openConfigFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        // 直接返回得到的 error,不做任何处理
        return err
    }
    return nil
}
// 自定义 error
type OpenErr struct {
    Err error  // 存放原始 error
}

// 实现 error 接口
func (*OpenErr) Error() string {
    return "can not open the specific file"
}

func openConfigFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return &OpenErr{Err:err}
    }
    return nil
}

上述四种方法各有千秋。如方法一,errors.New 会返回一个新的 error,其存放的数据就是我们传入的 text(“can not open the specific file”)。采用这种方式通常是为了告诉调用者出错了,但实际的错误细节不愿暴露。对调用者来说,他可能不太关心出了什么错,只在乎有没有出错。

方法二跟方法一相同,也会隐藏原始错误(这里指错误类型),但通常会将原始错误的字符串说明一起返回。在该处理方式中,“can not open the specific file” 为额外提示语,“reason: %v” 显示错误细节。一般来讲,调用 fmt.Errorf 更大几率是要产生一个给人看的错误,而不是让代码去解析(尽管并非不能)。

方法三,通常是函数调用者需要根据 error 的实际类型,或实际值,确定下一步的执行策略。也就是说它需要解析 error,所以函数返回“原味” error。这样做的缺点是,没办法对 error 添加额外信息。

方法四可以认为是上述三种方法的集合,既可以保留原始 error,还可以添加额外信息。通过这些元数据随意组合,调用者想要的样子它都有。缺点就是代码要多写一些。

如何检查 error

现在 error 有了,我们应该如何检查错误呢?在 1.13 之前,常见有:1. 比较值;2. 比较类型。

拿官方源码举例更具说服力。我们先来看看比较值。

func (db *DB) QueryContext(ctx context.Context, 
                           query string, 
                           args ...interface{}) (*Rows, error) {
    var rows *Rows
    var err error
    for i := 0; i < maxBadConnRetries; i++ {
        rows, err = db.query(ctx, query, args, cachedOrNewConn)
        if err != driver.ErrBadConn {  // 比较值
            break
        }
    }
    if err == driver.ErrBadConn {  // 比较值
        return db.query(ctx, query, args, alwaysNewConn)
    }
    return rows, err
}

func (db *DB) Query(query string, 
                    args ...interface{}) (*Rows, error) {
    return db.QueryContext(context.Background(), query, args...)
}
var ErrBadConn = errors.New("driver: bad connection")

上述是 sql.Query 的源码,确定处理 ErrBadConn 错误,就是通过比较值实现的。

比较类型处理 error 在 Go 源码中更常用,主要是这种处理方式更灵活,错误信息更丰富。

我们先写一个 demo,代码的主要目的是访问某网站。访问过程中可能出现各种异常,而我们只处理超时异常。

// http 客户端
client := &http.Client{
    Timeout:       3 * time.Second,  // 3 秒超时
}
// 请求不存在的 url
_, err := client.Get("http://www.meiyuouzhegewangzhan.com")
if err != nil {
    if os.IsTimeout(err) { // 超时 err
        fmt.Println("timeout")
    } else {  // 其他 err
        fmt.Println("other errors")
    }
}

从上得知,判断一个 error 是否是超时异常,需要调用 os.IsTimeout,源码如下。

func IsTimeout(err error) bool {
    // 类型断言
    terr, ok := underlyingError(err).(timeout)
    return ok && terr.Timeout()
}

func underlyingError(err error) error {
   // 返回实际 err
    switch err := err.(type) {
    case *PathError:
        return err.Err
    case *LinkError:
        return err.Err
    case *SyscallError:
        return err.Err
    }
    // 如果没有潜在 err, 返回自身
    return err
}

不论是 IsTimeout 还是 underlyingError 都借助类型比较实现对不同错误类型做处理。另外,继续跟进 PathError、LinkError、SysCallError 你会发现,这类 error 都是之前提到的第四种方法。它们包装了原始 error,但又会在一定场合下把它取出来(如上述代码中的 err.Err)。

1.13+ 如何检查 error

不得不承认,过去检查 error 的方式有点麻烦,尤其是在 error 被包装过多时。假设一个 error 被包装了 3 层,此时又需要检查最里层的 error,意味着代码需要这样写:

e1, _ := e.(Err1)
e2, _ := e1.(Err2)
e3, _ := e2.(Err3)
if e3 == targetErr {
    // handle
}

为了避免这种麻烦,1.13 开始,Go 提供了两个方法,用于检查链式中的 error。分别是比较值的 errors.Is,以及比较类型的 errors.As。想支持 error 链的检查,要求结构体(或其他错误类型)实现匿名接口:interface { Unwrap() error }

Unwrap

Unwrap 接口很好理解,用于返回结构体中的原始 error。拿 PathError 的源码举例:

type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Unwrap() error { return e.Err }

errors.Is

errors.Is 通过值比较确定错误,这里有易错点。由于许多 error 是指针类型,而指针类型的比较是:比较指针变量存放的地址是否相同。用代码解释更容易理解:

err1 := errors.New("error")
err2 := errors.New("error")

// 指针比较
fmt.Println(err1 == err2) // false

// 值比较
err1Elem := reflect.ValueOf(err1).Elem().Interface()
err2Elem := reflect.ValueOf(err2).Elem().Interface()
fmt.Println(err1Elem == err2Elem) // true

err1 与 err2 本质上可以认为是一个 error,连错误信息都相同,但直接比较会得到 false。这是因为 err1 与 err2 有不同的地址。第二种方式则是将两个指针指向的结构体取出来,比较两个结构体值,从而获得 true 的结果。

官方包对这类问题的处理常常是,定义一组错误变量,在之后的整个程序运行期间都使用已经赋值的错误变量——确保同一类错误的指针总是指向同一个地址

// go 源码
var (
    // ErrInvalid indicates an invalid argument.
    // Methods on File will return this error when the receiver is nil.
    ErrInvalid = errInvalid() // "invalid argument"

    ErrPermission = errPermission() // "permission denied"
    ErrExist      = errExist()      // "file already exists"
    ErrNotExist   = errNotExist()   // "file does not exist"
    ErrClosed     = errClosed()     // "file already closed"
    ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
)

func errInvalid() error    { return oserror.ErrInvalid }
func errPermission() error { return oserror.ErrPermission }
func errExist() error      { return oserror.ErrExist }
func errNotExist() error   { return oserror.ErrNotExist }
func errClosed() error     { return oserror.ErrClosed }
func errNoDeadline() error { return poll.ErrNoDeadline }

判断文件是否存在的两种方式:

var err error
f, err := os.Open("不存在文件")
defer f.Close()

// 方法1
if os.IsNotExist(err) { // 进入 if stmt
    fmt.Println("文件不存在")
}
// 方法2
if errors.Is(err, os.ErrNotExist) { // 进入 if stmt
    fmt.Println("文件不存在")
}

所以根据以上特性,errors.Is 几乎不能处理 error 链。聪明的官方库怎么会想不到这点,解决方案要从源码找:

// go 源码
func Is(err, target error) bool {
    ...
    isComparable := reflectlite.TypeOf(target).Comparable()
    for {
        // 值比较
        if isComparable && err == target {
            return true
        }
        // 调用 Is 比较
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        ...
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

errors.Is 会先进行值比较,如果失败,那就有 Is 调用 Is,所以我们可以给自定义的 error 实现 Is 方法,用于填写 error 是否相等的处理逻辑。

// Err1
type Err1 struct {
    Err error
}
func (e *Err1) Error() string { return "err1" }
func (e *Err1) Unwrap() error { return e.Err }
func (e * Err1) Is(other error) bool {
    v1 := reflect.ValueOf(e)
    v2 := reflect.ValueOf(other)
    // 如果是空指针直接返回
    if v1.IsNil() || v2.IsNil() {
        return false
    }
    // 取出指针指向的变量
    v1 = v1.Elem()
    if v2.Kind() == reflect.Ptr {
        v2 = v2.Elem()
    }
    return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}

// Err2
type Err2 struct {
    Err error
}
func (e *Err2) Error() string { return "err2" }
func (e *Err2) Unwrap() error { return e.Err }
func (e *Err2) Is(other error) bool {
    v1 := reflect.ValueOf(e)
    v2 := reflect.ValueOf(other)
    // 如果是空指针直接返回
    if v1.IsNil() || v2.IsNil() {
        return false
    }
    // 取出指针指向的变量
    v1 = v1.Elem()
    if v2.Kind() == reflect.Ptr {
        v2 = v2.Elem()
    }
    return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}

// Err3
type Err3 struct {
    Err error
}
func (e *Err3) Error() string { return "err3" }
func (e *Err3) Unwrap() error { return e.Err }
func (e *Err3) Is(other error) bool {
    v1 := reflect.ValueOf(e)
    v2 := reflect.ValueOf(other)
    // 如果是空指针直接返回
    if v1.IsNil() || v2.IsNil() {
        return false
    }
    // 取出指针指向的变量
    v1 = v1.Elem()
    if v2.Kind() == reflect.Ptr {
        v2 = v2.Elem()
    }
    return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}

// 产生 error
func genErr() error {
    return &Err1{
        Err: &Err2{
            Err: &Err3{
                Err: nil,
            },
        },
    }
}

func main() {
    err := genErr()
    err3 := &Err3{Err:nil}
    fmt.Println(errors.Is(err, err3))
}

代码明显累赘起来,往下看,还有更优雅的办法。

errors.As

当存在错误链时,我们更倾向于用类型定位错误,使用 errors.As 而不是 errors.Is。这样做也能避免冗余的 Is 方法。

// Err1
type Err1 struct {
    Err error
}
func (e *Err1) Error() string { return "err1" }
func (e *Err1) Unwrap() error { return e.Err }

// Err2
type Err2 struct {
    Err error
}
func (e *Err2) Error() string { return "err2" }
func (e *Err2) Unwrap() error { return e.Err }

// Err3
type Err3 struct {
    Err error
}
func (e *Err3) Error() string { return "err3" }
func (e *Err3) Unwrap() error { return e.Err }

// 产生 error
func genErr() error {
    return &Err1{
        Err: &Err2{
            Err: &Err3{
                Err: nil,
            },
        },
    }
}

func main() {
    err := genErr()
    var err3 *Err3
    fmt.Println(errors.As(err, &err3)) // 第二个参数要求是 指针的地址
}

占位符 %w

大多数情况下我们用不着大动干戈的自定义错误类型,而喜欢采用第二种方式 fmt.Errorf。从 1.13 开始,使用 %w 占位符会把原始 error 包裹起来放到 err 字段里,并构建一个新的 error 字符串放到 msg 中,对应的结构体就是 wrapError:

type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

也就是说,以下用法不会丢失原始 error,反而会构建 error 链,方便 errors.Is 与 errors.As 进行错误检查。

if _, err := os.Open("xxx.txt") {
    return fmt.Errorf("failed open, reason: %w", err)
}

感谢



本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论