Go 1.13 中errors包有了一些变化,这些变化是为了更好地支持Go的错误处理提案。Go 1.20中也增加了一个新方法,这个新方法可以代替第三方的库处理多个error,这篇文章将介绍这些变化。
因为原来的Go的errors中的内容非常的简单,可能会导致大家轻视这个包,对于新的变化不是那么的关注。让我们一一介绍这些新的方法。
Unwrap
如果一个err实现了Unwrap
函数,那么errors.Unwrap
会返回这个err的unwrap
方法的结果,否则返回nil。 一般标准的error都没有实现Unwrap
方法,比如io.EOF
, 但是也有一小部分的error实现了Unwrap
方法,比如os.PathError
,os.LinkError
、os.SyscallError
、net.OpError
、net.DNSConfigError
等等。
比如下面的代码:
1
2
3
fmt.Println(errors.Unwrap(io.EOF))
_, err := net.Dial("tcp" , "invalid.address:80" )
fmt.Println(errors.Unwrap(err))
第一行因为io.EOF
没有Unwrap
方法,所以输出nil。 net.Dial失败返回的err是*net.OpError
,它实现了Unwrap
方法,返回更底层的*net.DNSError
,所以第二行输出为lookup invalid.address: no such host
。
最常用的,我们使用fmt.Errorf
+ %w
包装一个error,比如下面的代码:
1
2
3
4
5
e1 := fmt.Errorf("e1: %w" , io.EOF)
e2 := fmt.Errorf("e2: %w + %w" , e1, io.ErrClosedPipe)
e3 := fmt.Errorf("e3: %w" , e2)
e4 := fmt.Errorf("e4: %w" , e3)
fmt.Println(errors.Unwrap(e4))
这段代码逐层进行了包装,最后的e4
包含了所有的error,我们可以通过errors.Unwrap
逐层进行解包,直到最底层的error。 fmt.Errorf可以1一次包装多个error,比如上面的e2
,它包含了e1
和io.ErrClosedPipe
两个error。
我们常常在多层调用的时候,把最底层的error逐层包装传递上去,这个时候我们可以使用fmt.Errorf
+ %w
包装error。 在最高层处理error的时候,再逐层Unwrap
解开error,逐层处理。
Is
Is
函数检查error的树中是否包含指定的目标error。
啥是error的树 ? 一个error的数包括它本身,以及通过Unwrap
方法逐层解开的error。 error的Unwrap
方法的返回值,可能是单个error,也可能是是多个error,在返回多个error的时候,会采用深度优先的方式进行遍历检查,寻找目标error。
怎么才算找到目标error呢?一种情况就是此err就是目标error,这没有什么好说的,第二种就是此err实现了Is(err)
方法,把目标err扔进Is
方法返回true。
所以从功能上看Is
函数其实叫做Has
函数更贴切些。
下面是一个例子:
1
2
3
4
5
6
7
e1 := fmt.Errorf("e1: %w" , io.EOF)
e2 := fmt.Errorf("e2: %w + %w" , e1, io.ErrClosedPipe)
e3 := fmt.Errorf("e3: %w" , e2)
e4 := fmt.Errorf("e4: %w" , e3)
fmt.Println(errors.Is(e4, io.EOF))
fmt.Println(errors.Is(e4, io.ErrClosedPipe))
fmt.Println(errors.Is(e4, io.ErrUnexpectedEOF))
As
Is
是遍历error的数,检查是否包含目标error。As
是遍历error的数,检查每一个error,看看是否可以把从error赋值给目标变量,如果是,则返回true,并且目标变量已赋值,否则返回false。
下面这个例子,我们可以看到As
的用法:
1
2
3
4
5
6
7
8
if _, err := os.Open("non-existing" ); err != nil {
var pathError *fs.PathError
if errors.As(err, &pathError) {
fmt.Println("failed at path:" , pathError.Path)
} else {
fmt.Println(err)
}
}
如果os.Open返回的error的树中包含*fs.PathError
,那么errors.As
会把这个error赋值给pathError
变量,并且返回true,否则返回false。 我们这个例子正好制造的就是文件不存在的error,所以它会输出:failed at path: non-existing
经常常犯的一个错误就是我们使用一个error
变量作为As
的第二个参数。下面这个例子tmp就是error接口类型,所以origin可以直接赋值给tmp,所以errors.As
返回true,并且tmp的值就是origin的值。
1
2
3
4
5
6
var origin = fmt.Errorf("error: %w" , io.EOF)
var tmp = io.ErrClosedPipe
if errors.As(origin, &tmp) {
fmt.Println(tmp)
}
As
使用起来总是那么别别扭扭,每次总得声明一个变量,然后把这个变量传递给As
函数,在Go支持泛型之后,As
应该可以简化成如下的方式:
1
func As[T error](err error) (T, bool )
但是,Go不会修改这个导致不兼容的API,所以我们只能继续保留As
函数,增加一个新的函数是一个可行的方法,无论它叫做IsA
、AsOf
还是AsTarget
或者其他。
如果你已经掌握了Go的泛型,你可以自己实现一个As
函数,比如下面的代码:
1
2
3
4
5
6
7
8
9
func AsA[T error](err error) (T, bool ) {
var isErr T
if errors.As(err, &isErr) {
return isErr, true
}
var zero T
return zero, false
}
写段测试代码,我们可以看到它的效果:
1
2
3
4
5
6
7
8
9
type MyError struct {}
func (*MyError) Error() string { return "MyError" }
func main() {
var err error = fmt.Errorf("error: %w" , &MyError{})
m, ok := AsA[*MyError](err)
fmt.Println(m, ok)
}
大家在#51945 讨论了一段时间,又是无疾而终了。
Join
在我们的项目中,有时候需要处理多个error,比如下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (s *Server) Serve() error {
var errs []error
if err := s.init(); err != nil {
errs = append (errs, err)
}
if err := s.start(); err != nil {
errs = append (errs, err)
}
if err := s.stop(); err != nil {
errs = append (errs, err)
}
if len (errs) > 0 {
return fmt.Errorf("server error: %v" , errs)
}
return nil
}
这段代码中,我们需要处理三个error,如果有一个error不为nil,那么我们就返回errs。 当然,为了处理多个errors情况,先前,有很多的第三方库可以供我们使用,比如
go.uber.org/multierr
github.com/hashicorp/go-multierror
github.com/cockroachdb/errors
但是现在,你不用再造轮子或者使用第三方库了,因为Go 1.20中增加了errors.Join
函数,它可以把多个error合并成一个error,比如下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
var e1 = io.EOF
var e2 = io.ErrClosedPipe
var e3 = io.ErrNoProgress
var e4 = io.ErrShortBuffer
_, e5 := net.Dial("tcp" , "invalid.address:80" )
e6 := os.Remove("/path/to/nonexistent/file" )
var e = errors.Join(e1, e2)
e = errors.Join(e, e3)
e = errors.Join(e, e4)
e = errors.Join(e, e5)
e = errors.Join(e, e6)
fmt.Println(e.Error())
fmt.Println(errors.Unwrap(e))
fmt.Println(errors.Is(e, e6))
fmt.Println(errors.Is(e, e3))
fmt.Println(errors.Is(e, e1))
你可以使用Is
判断是否包含某个error,或者使用As
提取出目标error。