查看原文
其他

Go1.20 继续小修小补 errors 库。。。

陈煎鱼 脑子进煎鱼了 2022-12-13

大家好,我是煎鱼。

Go 的错误处理机制一直是无数人提了又争,被拒了又提的地方。最近 Go1.20 即将发布,针对 errors 标准库,有一个新的小修小补优化(wrapping multiple errors)。

今天来学习这个三顾茅庐最终不怎么成功的阉割版提案。

回顾 Go1.13 改进 errors

在 Go1.13 中,errors 标准库引入了 Wrapping Error 的概念,并增加了 Is/As/Unwarp 三个方法,用于对所返回的错误进行二次处理和识别。

简单来讲,Go 的 error 可以嵌套了,提供了三个配套的方法。例子:

func main() {
 e := errors.New("脑子进煎鱼了")
 w := fmt.Errorf("快抓住:%w", e)
 fmt.Println(w)
 fmt.Println(errors.Unwrap(w))
}

输出结果:

$ go run main.go
快抓住:脑子进煎鱼了
脑子进煎鱼了

在上述代码中,变量 w 就是一个嵌套一层的 error。最外层是 “快抓住:”,此处调用 %w 意味着 Wrapping Error 的嵌套生成。因此最终输出了 “快抓住:脑子进煎鱼了”。

需要注意的是,Go 并没有提供 Warp 方法,而是直接扩展了 fmt.Errorf 方法。而下方的输出由于直接调用了 errors.Unwarp 方法,因此将 “取” 出一层嵌套,最终直接输出 “脑子进煎鱼了”。

对 Wrapping Error 有了基本理解后,我们简单介绍一下三个配套方法:

func Is(err, target error) bool
func As(err error, target interface{}) bool
func Unwrap(err error) error

errors.Is

方法签名:

func Is(err, target error) bool

方法例子:

func main() {
 if _, err := os.Open("non-existing"); err != nil {
  if errors.Is(err, os.ErrNotExist) {
   fmt.Println("file does not exist")
  } else {
   fmt.Println(err)
  }
 }

}

errors.Is 方法的作用是判断所传入的 err 和 target 是否同一类型,如果是则返回 true。

errors.As

方法签名:

func As(err error, target interface{}) bool

方法例子:

func main() {
 if _, err := os.Open("non-existing"); err != nil {
  var pathError *os.PathError
  if errors.As(err, &pathError) {
   fmt.Println("Failed at path:", pathError.Path)
  } else {
   fmt.Println(err)
  }
 }

}

errors.As 方法的作用是从 err 错误链中识别和 target 相同的类型,如果可以赋值,则返回 true。

errors.Unwarp

方法签名:

func Unwrap(err error) error

方法例子:

func main() {
 e := errors.New("脑子进煎鱼了")
 w := fmt.Errorf("快抓住:%w", e)
 fmt.Println(w)
 fmt.Println(errors.Unwrap(w))
}

该方法的作用是将嵌套的 error 解析出来,若存在多级嵌套则需要调用多次 Unwarp 方法。

问题在哪里

在 Go1.13 后,我们可以通过 fmt.Errorf 方法的把多个错误存进错误树中。

Errorf 方法内部代码如下:

func Errorf(format string, a ...any) error {
 ...
 var err error
 if p.wrappedErr == nil {
  err = errors.New(s)
 } else {
  err = &wrapError{s, p.wrappedErr}
 }
 p.free()
 return err
}

type wrapError struct {
 msg string
 err error
}

简单来讲,就是基于 wrapError 结构体实现了 Error interface,然后一层层往上套 error ,形成了错误树。

这看上去,一切都很美好,有个场景没有被考虑在内...如果有多个错误怎么办,又或是想将多个错误封装成一个,想自定义呢?这得咋整?

按逻辑来看,取出来的得一个个 Unwrap,再根据诉求去自己写自定义逻辑。这是比较麻烦的,API 没有充分提供帮助。

新提案

之前有提过类似的提案,可惜惨遭拒绝了。@Damien Neil 大佬熟络 Go 团队的流程、规范、风格,再度提出《errors: add support for wrapping multiple errors[1]》,挑战争议领域。

在诸多让步和讨论后,接纳了一个错误可以封装多个错误的特性,方案是原 Go1.13 API 的修改和 Go1.20 新增 errors.Join 方法和配套的方法改造。

Unwrap 函数将支持会封装多个错误:

Unwrap() []error

术语从 “错误链” 修改为 “错误树”。配套方法 errors.Is、errors.As、fmt.Errorf 都进行了改造。

对应如下:

  • errors.Is:如果能够匹配上任何错误,则返回 true。
  • errors.As:返回第一个匹配的错误。
  • fmt.Errorf:将会把多个错误封装在用户定义的布局中。

新 API Join 函数签名如下:

func Join(errs ...error) error 

对应的例子:

func main() {
 err1 := errors.New("err1")
 err2 := errors.New("err2")
 err := errors.Join(err1, err2)
 fmt.Println(err)
 if errors.Is(err, err1) {
  fmt.Println("err is err1")
 }
 if errors.Is(err, err2) {
  fmt.Println("err is err2")
 }
}

输出结果:

err1
err2
err is err1
err is err2

被 Join 的多个 error 默认将会通过换行符 \n 进行分隔来组装。

核心就是通过新增的 errors.Join 方法实现多个错误封装到一个错误中。方便了你去做多个错误的一次性提取,如果需要自定义错误,那就要再自己开发。

社区内也有对多个错误支撑的比较好的,有需要的小伙伴可以再看看。以下是比较有名的三个库:

  • hashicorp/go-multierror[2]
  • go.uber.org/multierr[3]
  • tailscale.com/util/multierr[4]

总结

Go 错误处理已经做了多次补丁的补全了,虽然这次主要是支持了 wrapping multiple errors,起码也是能够解决个别场景。

像是前两年,就有同学做表单校验,在内部系统,想把 errors 一次性全部返回出来的,结果 validate []error 只支持返回第一条,也没办法简单的一次性提取,麻烦的很。

Go1.20 将会发布本文提到的新提案,修修补补又从 1.13 到 1.20。

推荐阅读

参考资料

[1]

errors: add support for wrapping multiple errors: https://github.com/golang/go/issues/53435

[2]

hashicorp/go-multierror: https://pkg.go.dev/github.com/hashicorp/go-multierror

[3]

go.uber.org/multierr: https://pkg.go.dev/go.uber.org/multierr

[4]

tailscale.com/util/multierr: https://pkg.go.dev/tailscale.com/util/multierr


关注和加煎鱼微信,

一手消息和知识,拉你进技术交流群👇



你好,我是煎鱼,出版过 Go 畅销书《Go 语言编程之旅》,再到获得 GOP(Go 领域最有观点专家)荣誉,点击蓝字查看我的出书之路

日常分享高质量文章,输出 Go 面试、工作经验、架构设计,加微信拉读者交流群,和大家交流!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存