Go语言中常见100问题-#51 Comparing an error value inaccurately

2022年4月5日 281点热度 0人点赞 0条评论

如何正确地通过错误值对error进行比较是值得讨论的一个问题,可以类比Go语言中常见100问题-#50 comparing an error type。首先定义全局的哨兵错误,然后来分析如何通过错误值进行比较。

下面定义了一个全局的error,通常error变量命名以Err开头,后面是错误类型. 哨兵error期望描述的是一个预期的错误,下面以SQL库为例进行说明。

import "errors"

var ErrFoo = errors.New("foo")

设计一个查询数据库的Query方法,该方法返回结果是一个rows切片。在遇到查询结果为空的时候,怎么处理呢?有两个处理方法:

  • 返回一个特殊的标记值,返回nil切片(像标准库中的strings.Index方法,如果子串不存在,返回-1表示)
  • 返回一个具体的错误,调用方检查返回的error来进行判断

我们关注第二种方法,如果查询的结果为空,返回一个具体的错误。这是一种预期的问题,返回给调用方一个预期的错误用以区分这种情况。然而,在某些情况下,有些错误是难以提前确定的,像网络连接错误。我们并不是不想处理这种错误,而是因为它反映的是不同含义的问题。

在Go标准库中,可以看到不少这种通过哨兵标记错误的例子。

  • sql.ErrNoRows: 查询数据库数据为空的时候返回(就是前面说的例子)
  • io.EOF:io.Reader在没有输入数据的时候返回

上面是哨兵error想表达的一般原则,返回调用方期望检查的预期错误。因此,有以下准则:

  • 预期的error应该设计成值(哨兵error):var ErrFoo=errors.New("foo")
  • 无法预期的error设计成类型(通过type判断):type BarError struct{ … }, BarError类型实现error接口

现在分析一个共性的错误。如何比较error? 像下面的代码,可以使用==操作符

err := query()
if err != nil {
        if err == sql.ErrNoRows {
                // ...
        } else {
                // ...
        }
}

然而,在前面的问题Go语言中常见100问题-#49 wrap error中,error可以被包装。如果sql.ErrNoRows被使用fmt.Errorf+%w包装,err==sql.ErrNoRows将会永远不成立。Go1.13版本提供了解决方法。通过errors.As来检测wrap error是否是某种类型,同理,通过errors.Is来检测wrap error的值是否是某个具体的错误。改写后的代码为:

err := query()
if err != nil {
        if errors.Is(err, sql.ErrNoRows) {
                // ...
        } else {
                // ...
        }
}

使用errors.Is替换==比较错误是最佳的做法,因为它也能处理wrap error的情况。

总结,在程序中使用了fmt.Errorf+%w来包装错误,在检查error的时候不能用==,而是用errors.Iserrors.Is会递归的unwrap error检查每层中的error是否是要比较的。

28190Go语言中常见100问题-#51 Comparing an error value inaccurately

这个人很懒,什么都没留下

文章评论