Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go1.13 errors包变化

Go 1.13中errors包的新变化示例解析

作者:晁岳攀(鸟窝) 鸟窝聊技术

这篇文章主要为大家介绍了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.PathErroros.LinkErroros.SyscallErrornet.OpErrornet.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,它包含了e1io.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函数,增加一个新的函数是一个可行的方法,无论它叫做IsAAsOf还是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包变化的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文