浅谈 Go 语言的错误
Go 一直把错误作为一个普通的值,一个包含一段文本信息的类型为错误的值,而这在 1.13 版本之后发生了改变。
1.13 版本之前
Go 中任意类型只要实现了 error
接口的都可以称为错误。
type error interface {
Error() string
}
程序中通常会直接用 errors.New
生成错误,调用者拿到错误的时候可以直接用 ==
做错误判断:
var ErrNotFound = errors.New("not found")
if err == ErrNotFound {
// 错误处理
}
当简单的错误不能满足业务要求时,也会自己定义 struct
来实现 error
接口,然后需要通过类型断言 (type assertion or type switch) 做错误判断:
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string { return e.Name + ": not found" }
if e, ok := err.(*NotFoundError); ok {
// 错误处理
}
当错误要传递额外信息时,通常是用 fmt.Errorf
来包含原来的错误:
if err != nil {
return fmt.Errorf("登陆失败: %v", err)
}
但上述方法只能包含原有错误中的文本信息 (message),失去了原有错误的其他信息,包括类型以及struct
其他自定义的字段。调用者也失去了处理原有错误的机会,只能基于文本做分析处理。
新的 API
问题出在了 fmt.Errorf
返回的时候失去了内部错误的信息,保留的文本信息在简单的 debug 场景下是够用了,到复杂系统中,这样的信息是远远不够的。
Go 在 1.13 版本中带来了 error wrapping,简单的说就是在错误传递的过程中,新生成的错误中可以包含完整的其他错误,就像洋葱一样的层层包裹。
fmt.Errorf
新的 %w
生成包裹的错误:
newErr := fmt.Errorf("access error: %w", permissionErr)
errors.Unwrap
剥开一层返回内部的错误,这个方法要求包裹的错误实现 Unwrap
方法,用 fmt.Errorf
生成的错误已经实现了该方法:
permissionErr := errors.Unwrap(newErr)
errors.Is
代替 ==
判断错误,会调用 errors.Unwrap
判断错误链上任意一个错误是否与目标错误相等:
// 类似于 if err == ErrNotFound {
if errors.Is(err, ErrNotFound) {
// 错误处理
}
errors.As
代替类型推断判断错误,会调用 errors.Unwrap
判断错误链上任意一个错误是否与目标错误类型相同,如果相同,将错误赋值给目标类型:
// 类似于 if e, ok := err.(*NotFoundError); ok {
var e *NotFoundError
if errors.As(err, &e) {
// 错误处理
}
扩展
可以通过实现 Is(err error) bool
方法扩展 errors.Is
的比较逻辑,考虑下面的例子
type Error struct {
Path string
User string
}
func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok {
return false
}
return (e.Path == t.Path || t.Path == "") &&
(e.User == t.User || t.User == "")
}
if errors.Is(err, &Error{User: "someuser"}) {
// err's User field is "someuser".
}
这样错误判断也有更强的自定义行为。errors.As
方法也是同样可以用 As
方法来扩展。
源码解析
以下是基于 go version go1.13.1 darwin/amd64 的源代码解析
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
}) // 判断错误有没有实现 Unwrap 方法
if !ok {
return nil // 没有实现返回 nil
}
return u.Unwrap() // 调用 Unwrap 方法返回内部错误
}
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target { // 如果能比较,并且 err 与 target 相等
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) { // 如果实现了 Is 方法,用 Is 来比较
return true
}
if err = Unwrap(err); err == nil { // Unwrap 返回内部错误
return false
}
}
}
func As(err error, target interface{}) bool {
if target == nil { // target 不能是 nil
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() { // target 不能是个空指针
panic("errors: target must be a non-nil pointer")
}
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) { // target 必须是接口或者是实现了 error 接口的 struct
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
if reflectlite.TypeOf(err).AssignableTo(targetType) { // https://golang.org/ref/spec#Assignability
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) { // 如果定义 As 方法,通过 As 方法转换
return true
}
err = Unwrap(err) // Unwrap 返回内部错误
}
return false
}
Reference
- https://blog.golang.org/go1.13-errors
- https://golang.org/ref/spec#Assignability
- https://github.com/golang/go/wiki/ErrorValueFAQ
- https://golang.org/doc/go1.13