Go 1.13中errors包的新变化示例解析
作者:晁岳攀(鸟窝) 鸟窝聊技术
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
等等。
比如下面的代码:
fmt.Println(errors.Unwrap(io.EOF)) // nil _, 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,比如下面的代码:
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)) // e3: e2: e1: EOF + io: read/write on closed pipe
这段代码逐层进行了包装,最后的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
函数更贴切些。
下面是一个例子:
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)) // true fmt.Println(errors.Is(e4, io.ErrClosedPipe)) // true fmt.Println(errors.Is(e4, io.ErrUnexpectedEOF)) // false
As
Is
是遍历 error 的数,检查是否包含目标 error。As
是遍历 error 的数,检查每一个 error,看看是否可以把从 error 赋值给目标变量,如果是,则返回 true,并且目标变量已赋值,否则返回 false。
下面这个例子,我们可以看到As
的用法:
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 的值。
var origin = fmt.Errorf("error: %w", io.EOF) var tmp = io.ErrClosedPipe if errors.As(origin, &tmp) { fmt.Println(tmp) // error: EOF }
As
使用起来总是那么别别扭扭,每次总得声明一个变量,然后把这个变量传递给As
函数,在 Go 支持泛型之后,As
应该可以简化成如下的方式:
func As[T error](err error "T error") (T, bool)
但是,Go 不会修改这个导致不兼容的 API,所以我们只能继续保留As
函数,增加一个新的函数是一个可行的方法,无论它叫做IsA
、AsOf
还是AsTarget
或者其他。
如果你已经掌握了 Go 的泛型,你可以自己实现一个As
函数,比如下面的代码:
func AsA[T error](err error "T error") (T, bool) { var isErr T if errors.As(err, &isErr) { return isErr, true } var zero T return zero, false }
写段测试代码,我们可以看到它的效果:
type MyError struct{} func (*MyError) Error() string { return "MyError" } func main() { var err error = fmt.Errorf("error: %w", &MyError{}) m, ok := AsA[*MyError](err "*MyError") // MyError does not implement error (Error method has pointer receiver) fmt.Println(m, ok) }
大家在#51945[1]讨论了一段时间,又是无疾而终了。
Join
在我们的项目中,有时候需要处理多个 error,比如下面的代码:
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,比如下面的代码:
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()) // 输出如下,每一个err一行 // // EOF // io: read/write on closed pipe // multiple Read calls return no data or error // short buffer // dial tcp: lookup invalid.address: no such host // remove /path/to/nonexistent/file: no such file or directory fmt.Println(errors.Unwrap(e)) // nil fmt.Println(errors.Is(e, e6)) //true fmt.Println(errors.Is(e, e3)) // true fmt.Println(errors.Is(e, e1)) // true
你可以使用Is
判断是否包含某个 error,或者使用As
提取出目标 error。
参考资料
[1]
#51945: https://github.com/golang/go/issues/51945
以上就是Go 1.13中errors包的新变化示例解析的详细内容,更多关于Go1.13 errors包变化的资料请关注脚本之家其它相关文章!