大家好,我是煎鱼。

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》,挑战争议领域。

在诸多让步和讨论后,接纳了一个错误可以封装多个错误的特性,方案是原 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 方法实现多个错误封装到一个错误中。方便了你去做多个错误的一次性提取,如果需要自定义错误,那就要再自己开发。

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

总结

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

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

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

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言可以看 Go 学习地图和路线,欢迎 Star 催更。

Go 图书系列

推荐阅读


煎鱼
8.4k 声望12.8k 粉丝