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
函数来对错误链中的错误进行提取和检视。