下面定义了一个全局的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.Is
, errors.Is
会递归的unwrap error检查每层中的error是否是要比较的。