Go 采用的是基于 错误值比较 的错误处理机制,即通过检查返回的 error 值来判断函数/方法是否执行成功,如果执行失败,也是通过该 error 值来携带具体的错误信息。相比于其他编程语言的 异常 错误处理机制,这种 值比较 机制更加简单,它要求在程序控制流程中显式关注和处理每个错误,这样的代码往往可读性更好,也符合 Go 追求简答的设计哲学。
标准库的 errors 包为 Go 的错误处理机制提供了基础支持,这篇文章将学习 errors 包的基本用法和原理。
error 接口
这种基于 值比较 的错误处理方式使得我们可以使用任意类型来表示错误信息,例如可以像 C 语言惯例那样使用 int 类型来表示错误:0 表示成功,非 0 表示错误,不同的错误值(通常称为错误码)表示不同错误类型。C 语言这种使用整型值来表示错误的方式虽然简单,但它毕竟不是语言标准,无法让不同代码遵守同一套约定。而且整型值能够携带的错误信息也很少。
为了能够在所有 Go 代码之间统一地表示错误,Go 语言定义了 error 接口类型,它的定义如下:
1 | type error interface { |
当我们的 Go 代码需要接收、处理、返回错误值时,可以统一使用 error 接口类型来表示错误。另一方面,我们可以根据自己的需要定义具体的错误类型,只要这些数据类型实现了 error 接口,那么这些类型的值都可以用于表示 错误。这样既保证了错误处理在不同 Go 代码之间的一致性,又赋予了 具体实现 灵活定义错误类型的自由。
这其实也是 面向接口 编程思想的体现。
errors.New 函数
errors 包内部定义了 errorString 类型,用于实现 error 接口:
1 | type errorString struct { |
errorString 类型是个非导出类型,通过 errors.New 函数创建一个该类型的实例:
1 | func New(text string) error { |
可以看到 errorString 的实现非常简单:
- 当调用
errors.New(string)创建errorString实例时,它保存下传入的错误字符串 - 当通过
Error()方法获取错误信息时,直接返回所保存的错误字符串
但是需要注意到,errors.New 函数每次都是返回一个新的 errorString 实例,因此当对错误值进行比较式,两个实例即使保存的错误字符串相同,它们也是不相同的:
1 | package main |
为了解决上述问题,一般的做法是提前通过 errros.New() 定义好各种错误,每次总是使用这些已经定义好错误实例值:
1 | package main |
errors.Join 函数
errors.Join() 函数可以将多个错误实例包装(wrap)成一个,参数中的 nil 会被忽略,如果所有错误实例值都是 nil,则 Join 函数也返回 nil。
1 | func Join(errs ...error) error |
Join 函数返回的错误实例类型是非导出类型 joinError 类型,它是以切片的形式保存所有的错误实例,该类型定义如下:
1 | type joinError struct { |
Join 函数的核心实现就是将参数中的所有非 nil 的 error 保存到 joinError 的 err 切片中,并返回 joinError 类型的实例。
joinError 类型实现了 Error 接口,其 Error() 方法就是调用 err 切片中每个对象的 Error() 方法,并以 \n 作为分隔符连接各个错误字符串,并返回连接后的结果。
错误包裹链
当通过 Join() 将一组错误包装成一个错误后,我们如何判断应该如何进行错误的 值比较 呢?例如假设通过 Join 函数将 A 和 B 包装成一个错误 AB,我们预期 AB 错误应该也属于 A 错误。这就涉及 Go 对错误链的支持了。
当代码调用层次比较深时,每一层可能都会构造自己的错误信息,同时我们又不希望丢失原有的错误上下文信息,这时就需要通过 错误包裹链 机制来提供 错误的包裹 功能了。错误包裹机制可以接受一个或一组错误,将其包裹成一个新的错误,并且可以从该新错误中提取原有错误信息。Join 函数、fmt.Errorf 的 %w 都提供错误包裹能力。
go 的 erros 包提供了 Is 和 As 函数来从 包裹错误 中检视、提取原始错误。这两个函数依赖的核心机制就是如下接口:
1 | interface{ Unwrap() error } |
errors.Is 函数
errors.Is 函数用于判断 err 错误是否和指定的 target 错误匹配,该函数会沿着 err 的包裹链,逐步 Unwrap(),判断其返回值是否和 target 相等。下面展示了 errors.Is 函数的实现:
1 | func Is(err, target error) bool { |
可以看到,errors.Is 函数的核心逻辑是:
- 如果 err 自己实现了
Is方法,首先调用其自己的 Is 方法进行判断 - 否则判断 err 是否实现了
Unwrap() error方法或Unwrap() []error方法。如果实现了这两个接口,则会调用Unwrap方法并对其返回的 err 进行判断 - 以上过程会不断递归进行,直至得到判断结果。如果无法继续判断下去,则返回 false
errors.As 函数
errors.As 函数 As 函数用于判断 err 错误是否可以当成 target 错误类型,如果是可以的话,会在 target 中保存该错误值并返回 true,否则返回 false。直接。即 errors.Is() 函数用于是错误值的比较,而 errors.As() 则是错误类型的比较。
1 | func As(err error, target any) bool |
As 函数的实现如下:
1 | As(err error, target any) bool { |
可以看到,errors.As 函数的逻辑类似于 errors.Is 函数:
- 通过反射中的
reflectlite.Type.AssignableTo方法判断 err 值是否可以赋值给 targetType - 如果 err 自己实现了
As方法,首先调用其自己的 As 方法进行判断 - 否则判断 err 是否实现了
Unwrap() error方法或Unwrap() []error方法。如果实现了这两个接口,则会调用Unwrap方法并对其返回的 err 进行判断 - 以上过程会不断递归进行,直至得到判断结果。如果无法继续判断下去,则返回 false
这里需要注意下 errors.As 函数的 target 必须是个非 nil 指针,且指针所指向类型必须是个接口类型或者实现了 Error() 方法的数据类型。
errors.Unwrap 函数
除了以上两个函数,errors 包还提供了 Unwrap 函数,可以直接尝试获取 err 所包裹的下一层错误。其实现非常简单:如果 err 实现了 Unwrap() error 方法,则直接调用该方法:
1 | func Unwrap(err error) error { |
特别注意,Unwrap 方法只判断了 err 是否实现了 Unwrap() error 方法,其并不会尝试调用 Unwrap() []error 方法。
简单示例
首先如下展示了 Join 函数的一个基本使用:
1 | package main |
可以看到 Join 函数也能够支持 错误包裹链,这正是因为其实现了 Unwrap() []error 方法
1 | func (e *joinError) Unwrap() []error { |
再来看一个自定义类型实现 错误包裹链 的例子:
1 | package main |
这个例子中,需要特别注意的是 errors.New() 函数的返回的是 error 接口类型,因此当调用 errors.As(wrap1, &err2) 时,其实是判断 wrap1 是否可以赋值给 error 接口类型,因此 err2 最后其实保存的是值其实是 wrap1 本身。
小结
这篇文章学习了 go 库 erros 包所提供的基础设施,包括 errors.New 函数、errors.Join 函数等。同时 errors 包通过 Unwrap 接口实现了对 错误包裹链 的支持,我们可以通过 errors.Unwrap、errors.Is、errors.As 函数来对错误链中的错误进行提取和检视。