在 Go 1.13 以前,标准库中的 error 包仅包含一个 New
函数:
package errors
// New 从给定格式的字符串中创建一个错误
func New(text string) error {
return &errorString{text}
}
// errorString 是一个平凡的 error 实现
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
另一个与 error 相关的函数则位于 fmt
包中:
func Errorf(format string, a …interface{}) error {
return errors.New(Sprintf(format, a…))
}
其形式也就仅仅只是对 errors.New
加上 fmt.Sprintf
的一个简单封装。 如上一节中对错误处理的讨论,这种依靠字符串进行错误定义的方式的可处理性几乎为零,会在上下文之间引入强依赖。
从 Go 1.13 起,Go 在 errors
包中引入了一系列新的 API 来增强错误检查的手段。
17.2.1 errors.Unwrap
Unwrap
的目的是将一个已被 fmt
包装过的 error
进行拆封,其实现逻辑利用了 .(type)
断言:
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
}) // 断言 err 实现了 Unwrap 方法
if !ok {
return nil // 如果没有实现,则拆封失败
}
return u.Unwrap() // 否则调用实现的 Unwrap 方法
}
17.2.2 errors.Is
与 errors.As
Is
用于检查当前的两个 err 是否相等。之所以需要这个函数是因为一个错误可能被包装了多层, 那么我们需要支持这个错误在包装过多层后的判断,因而可想而知在实现上需要一个 for 循环对其进行 Unwrap:
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
// 如果 target err 是可比较的,则直接进行比较
if isComparable && err == target {
return true
}
// 如果 err 实现了 Is 方法,则调用其实现进行判断
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 否则,对 err 进行 Unwrap
if err = Unwrap(err); err == nil {
return false
}
// 如果 Unwrap 成功,则继续判断
}
}
可见 errors.Is
方法的目的是替换如下形式的错误检查:
if err == io.ErrUnexpectedEOF {
// … 处理错误
}
=>
if errors.Is(err, io.ErrUnexpectedEOF) {
// … 处理错误
}As
的实现与 Is
基本相同,不同之处在于 As
的目的是将某个 err
给拆封 到 target
中,因此对于一个错误链而言,需要一个循环不断对错误进行 Unwrap
, 当错误实现 As
方法时,直接调用 As
:
func As(err error, target interface{}) bool {
if target == nil {
panic(“errors: target cannot be nil”)
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic(“errors: target must be a non-nil pointer”)
}
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
panic(“errors: target must be interface or implement error”)
}
targetType := typ.Elem()
for err != nil {
// 若可直接将 err 拆封到 target
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
// 判断 err 是否实现 As 方法,若已实现则直接调用
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
// 否则对错误链进行 Unwrap
err = Unwrap(err)
}
return false
}
var errorType = reflectlite.TypeOf((error)(nil)).Elem()
由于错误链的存在, errors.As
方法的目的是替换如下形式的错误检查:
if e, ok := err.(*os.PathError); ok {
// … 处理错误
}
=>
var e *os.PathError
if errors.As(err, &e) {
// … 处理错误
}
17.2.3 fmt.Errorf
中的 %w
fmt.Errorf
函数增加了一个 %w
动词,允许对一个错误进行封装:
package fmt
import “errors”
func Errorf(format string, a …interface{}) error {
p := newPrinter()
p.wrapErrs = true
p.doPrintf(format, a)
s := string(p.buf)
var err error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
在 Go 1.13 的 Errorf
的实现中,将需要包装的 err
包装为一个 wrapError
, 包含错误消息本身与对错误的封装:
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e wrapError) Unwrap() error { // 支持 errors.Unwrap 方法
return e.err
}
错误的封装利用了 pp
结构:
type pp struct {
buf buffer // []byte
…
// 当格式化过程中可能包含 %w 动词时,设置为 true
wrapErrs bool
// wrappedErr 记录了 %w 动词的 err
wrappedErr error
}
我们也并不关心 newPrinter
和 doPrintf
具体做了什么,我们只想知道如果出现 %w
动词,具体如何对错误进行封装, 可以观察到 doPrintf
函数内部调用了 printArg
,而具体处理一个 error 类型时,会调用 handleMethods
方法来处理具体的动词, 对于 %w
的处理就在这里:
func (p pp) handleMethods(verb rune) (handled bool) {
if p.erroring {
return
}
if verb == ‘w’ {
// 判断与 %w 对应的值是否为 error 类型,否则处理为错误的动词组合
err, ok := p.arg.(error)
if !ok || !p.wrapErrs || p.wrappedErr != nil {
p.wrappedErr = nil
p.wrapErrs = false
p.badVerb(verb)
return true
}
// 保存 err,并将其转化为 %v 的情况
p.wrappedErr = err
verb = ‘v’
}
…
}
很明显关于 %w
这个动词的处理仅仅就是将 err 记录到 wrappedErr 这个变量中, 并将 verb 修改为 v 将其转化为 %v
动词进行后续的格式化处理。
17.2.4 小结
本节我们详细讨论了 errors
包的全部设计,它通过暴露 New
、Unwrap
、Is
和 As
四个接口完成了对复杂函数调用栈中使用 fmt.Errorf
层层封装的错误链条的封装和拆解过程,其中 New
负责原始错误的创建,Unwrap
允许对链表进行一次拆包,Is
提供了在复杂错误链中,判断错误类型的能力,As
则提供了将错误从错误链拆解到某个目标错误类型的能力。
进一步阅读的参考文献
- [Amsterdam, 2019] Jonathan Amsterdam. proposal: Go 2 error values. Jan 25, 2019. https://github.com/golang/go/issues/29934
- [Neil, 2019] Damien Neil. Go 1.13 lunch decision about error values. May 6, 2019. https://github.com/golang/go/issues/29934#issuecomment-489682919)
- [Cox, 2019] Russ Cox. Response, Response regarding “proposal: Go 2 error values”. May 7, 2019. https://github.com/golang/go/issues/29934#issuecomment-490087200)
- [Amsterdam and Mills, 2019] Jonathan Amsterdam and Bryan C. Mills. Error Values: Frequently Asked Questions. August 2019. https://github.com/golang/go/wiki/ErrorValueFAQ
- [Neil and Amsterdam, 2019] Damien Neil and Jonathan Amsterdam. Working with Errors in Go 1.13. October 17, 2019. https://blog.golang.org/go1.13-errors