大家好,我是煎鱼。
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 催更。