一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第21天,点击查看活动详情。
error 是 Golang 体系中的一个核心概念。在 Golang 中,函数可以返回多个值,我们经常会看到一个函数同时返回了 value 和 error。此时调用方应当根据场景处理 error。
今天我们来根据源码看看底层是怎么支持 error 机制的。
error 接口
在底层 error 本质是一个接口:
// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
Error() string
}
非常直接,只需要提供一个能返回【错误信息】字符串的函数,就算实现了 error 接口。
这里还是用了【声明接口】+【提供默认实现】的经典模式,在整体 Golang 的协作流程中,我们依赖的都是接口。开发者也可以非常低成本地自己实现一个自定义的 error。
自定义 error 实现
type ErrorWithCode struct {
msg string
code int
}
func (e *ErrorWithCode) Error() string {
return fmt.Sprintf("%s: code %d", e.msg, e.code)
}
func NewErrorWithCode(msg string, code int) error {
return &ErrorWithCode{
code: code,
msg: msg,
}
}
这是一个经典的 msg + code 的实现,msg 用于明文帮助RD判断问题,调用方看 code 即可。
errors 实现
如果你没有自定义的诉求,对于简单场景,可以使用 Golang 提供的 errors 包的能力,我们来看看。
package errors
// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
return &errorString{text}
}
// errorString is a trivial implementation of error.
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
errors 包借用 errorString 这个结构体提供了 error 接口的实现。通过 errors.New(),传入错误信息,就可以很简洁的创建一个 errorString。
fmt 实现
有时候我们也会用到 fmt.Errorf 来创建一个 error 接口的实现。区别在于,errors.New 只能接受一个已经组装好的字符串,而 fmt.Errorf 接受的是一个 format + 参数。对比一下:
myError := errors.New("only text")
param := "some string"
myFmtError := fmt.Errorf("text with param=%v", param)
使用 fmt.Errorf 创建出来的 error 接口实现我们来看一下:
package fmt
import "errors"
func Errorf(format string, a ...any) 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
}
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
其实底层的实现,被拆分出了两个:
- errors.New() : 此时底层还是 errors 包里的 errorString;
- wrapError
逻辑比较直接,问题的关键在于通过 fmt.Errorf 创建 error 时是否存在 %w 标记。
包装 error 的方式
很多时候我们并不是直接处理一个底层原始的 error。比如函数 A 调用了函数 B,函数 B 又调用了 函数 C。 从 函数 B 的角度看,当 C 抛 error 了,B 的流程无法继续,也需要继续抛 error 给 A。
但问题出来了,如果只是用常规的 fmt.Errorf("biz failed, call C err=%v", err)处理(此处 err 为 C 返回的错误),意味着你抛弃了原始的错误,你把一个 error interface 变成了【错误信息字符串】。
再往上层抛的时候,如果函数 A 需要甄别某一个 C 的错误,进行处理,要怎么办呢?
只能匹配字符串,或要求 B 也相应的定义一个同样语义的错误。这样其实很不方便。
有没有办法能直接把底层函数的 error 跨级别透传上去呢?我不希望这是个字符串,我希望给到 A 原始的错误,这样 A 还可以针对这些错误进行判断,断言。
方案其实在上面已经给出来了,这就是 fmt.Errorf 现在提供了 wrappError 的意义。
以前我们还需要依赖外部包
import "github.com/pkg/errors"
if err != nil {
return errors.Wrap(err, "some biz context")
}
现在使用最新的 Golang 版本,可以直接用
import "fmt"
if err != nil {
return fmt.Errorf("biz context: %w", err)
}
在 fmt 包的底层,会把 %w 识别出来,对 p.arg 断言为 error 接口,赋值给 p.wrappedErr
回到 Errorf 这里,如果上一步 p.wrappedErr 不为空,此时会返回新定义的 wrapError 结构体实现
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
这个 wrapError 的形式其实也是大家自定义 error 中常用的形式,错误信息 msg + 原始错误 error
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
errors 提供的工具函数
不管是 errors 包中的 errorString 结构,还是 fmt 包中的 wrapError 结构,两种实现都能满足大部分诉求。但我们还有一些常见的诉求:
判断两个 error 是否相等
以前我们经常用 err == someDefinedError 来进行比较。比较常见的是 GORM 第一版,针对没有找到数据的场景,我们会用 GORM 定义的一个 RecordNotFound error 来匹配当前的返回,如果是确实没有数据,可能我们就直接给上层返回 nil 而不是透传 error 了(语义上应该是空,而不是报错)
那么问题来了,我们前面有提到,error 是可能被包装的。事实上我们也不鼓励处理 error 的地方,无脑直接 return 给上层。比如下面这样:
data, err := executeBusinessLogic()
if err != nil {
return err
}
因为下层的错误,语义上来说并不一定要导致中间层也报错,而且上层的函数可能也不需要直接感知底层错误。
ok,既然有 wrap,那直接 == 的时候,肯定就没法匹配了,我们需要有办法,对 someDefinedError 能进行链式处理,如果自身不匹配,不要直接说不相等,而是看看自己 wrap 的 error 是否匹配。如果内层还有 wrap,也能持续追到底层。如果都不匹配,才要返回【不相等】的业务语义。
errors 包针对这种场景,提供的函数为 func Is(err, target error) bool,对于 err 的整条错误链都进行校验,同时也支持业务自定义 Is 函数。
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
if err = Unwrap(err); err == nil {
return false
}
}
}
这里也能看到,如果 err wrap 了个内层的错误,就在 for 循环里继续 Unwrap(解包),拿到内层的 error,继续判断。
解包 error
解包的逻辑相对是清晰的,注意 error 是个接口,你实现 Error() 方法就够了,也就意味着,从这个角度划分,Golang 中的 error 分为两类:
- 单层 error,只有一级,典型的案例就是 errors 包提供的 errorString 实现;
- 多层 error,支持 wrap,典型的案例是 fmt 包提供的 wrapError 实现。
对于单层 error,进行解包是没有意义的,它自己就是原始的错误。 对于多层 error,由于 error 接口并没有一个解包方法定义,需要去适配一个统一的规范,支持解包。
这就是 errors 包提供的 Unwrap 函数的意义:
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
逻辑非常简单,如果你是个支持多层 error 的实现,那么请提供对应的 Unwrap() error 方法,这样 errors.Unwrap 就能识别到,进而进行解包。
断言 error 接口为指定错误类型
一个常见的问题在于,error 毕竟是个接口,假设我们实现了业务自己的 error 结构,需要类型转化。应该怎么做?
在 Go 1.13 之前我们会这样(借用开篇的自定义 ErrorWithCode):
if e, ok := err.(*ErrorWithCode); ok {
fmt.Println(e.code)
}
现在,更加鼓励的方案是 errors 包提供的 func As(err error, target any) bool 函数
import "errors"
var e ErrorWithCode
if errors.As(err, e) {
fmt.Println(e.code)
}
经过 As 处理后,会将 err 的值转化到 e 中,可以直接使用。根据返回的 bool 判断是否断言成功。
我们来看一下源码实现:
func As(err error, target any) 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")
}
targetType := typ.Elem()
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
for err != nil {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err)
}
return false
}
可以看到,As 底层依赖反射来判断 target 是否是 Assignable 的,如果是,就通过 Set 方法写入。此外也同样支持了自定义的实现来提供自己的 As(any) bool 实现。
精华在于这里,基础的反射操作:
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
err == nil 的经典误区
这里简单重提一下,感兴趣的同学建议还是好好看看上一篇 Golang 中的 nil 用法解析
error 是个接口,接口的底层是 type + value,而 == nil 这类操作,只有在 type 和 value 都是空的,才会返回 true。
value 为空是开发者能感知到的,我们通常不会在这里犯错。但 type 是否为空,这一点是很 trick 的。
因为当你声明了一个变量类型,再包上 interface 时,这个动态类型已经确定,那么无论什么时候跟 nil 去 ==,都为 false。
所以,具体到 error 这种场景,我们的建议是:函数返回值是 error 接口时,如果你要返回一个 nil 的 error,请直接 return nil,而不是声明一个变量,然后直接返回。
return nil // 完美,不会有问题
var empty *ErrorWithCode
return empty // 表面上是 nil,因为你的返回值是 error 接口,会导致后续接口 == nil 时出问题