Go 语言中错误处理的演变
从 Go 语言发布的第一天起,错误处理就一直是 Go 语言中的重要话题。社区中有很多关于错误处理的争议和讨论,也有很多关于 Go 语言如何处理错误的建议。当然,Go 语言开发组也一直在努力改进。本文介绍 Go 语言中错误处理的演变过程。
more
Go1.0 - Go1.12 创建错误的三种基本方式
从 Go1.0 开始,Go 语言中就内置了 error 接口:
type error interface {
Error() string
}
一个 error 只需要实现方法 Error() string,它返回错误文本
也是从此时开始,Go 语言中一直都存在有基本的三种错误创建方式:
- 使用 errors 包的 New 函数:
errors 包提供了一个简单的函数 New,用于创建错误, 这个方法内部只是返回一个 errorString 类型的错误,没有任何多余项。
# src/pkg/errors/errors.go
func New(text string) error {
return &errorString{text}
}
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
创建一个简单错误:
err := errors.New("error message")
- 使用 fmt 包的 Errorf 函数:
fmt 包提供了一个函数 Errorf,用于创建格式化的错误字符串,这个方法内部也只是调用 errors.New 创建了一个简单错误。
src/pkg/fmt/print.go
# src/pkg/fmt/print.go
func Errorf(format string, a ...interface{}) error {
return errors.New(Sprintf(format, a...))
}
创建一个格式化文本错误:
err := fmt.Errorf("这是一个格式化的错误: %d", 42)
- 内置的错误不够用的时候,还可以自定义错误类型:
如果我们需要错误代码,可以定义一个实现了 error 接口的结构体来创建自定义错误类型。
type MyError struct {
Msg string
Code int
}
func (e *MyError) Error() string {
return fmt.Sprintf("错误信息: %s, 错误代码: %d", e.Msg, e.Code)
}
很明显,错误只是一个普通的值。处理错误只需通过使用 if 语句来检查错误是否为 nil:
if err != nil {
fmt.Println("error:", err)
}
Go 语言的内置错误,有些过于简单,仅仅包含一段错误文本。在实际应用中,许多时候应用为了解耦,会划分多个层次。底层发生的错误,通常会由上层进行处理,而在应用上层,我们处理错误时就需要知道错误发生的具体位置,以及错误的具体原因。我们不得不自定义错误类型,以便在应用中传递错误信息。
Go1.13 - Go1.19 上下文错误包装与错误连的支持
为了解决这个问题,从 Go1.13 开始,Go 语言引入了错误包装的概念。它允许将一个错误包装在另一个错误中,并且可以递归式包装,形成错误链,以便提供更多上下文信息。
fmt 包的 Errorf 函数做了升级,允许通过%w 格式化参数来将一个错误包装在另一个错误中,并附加额外的上下文信息:
# src/fmt/errors.go
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
}
wrapError 结构体实现了 error 接口,本身是一个错误,同时提供了 Unwarp()方法用于解包错误:
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
实际用法示例:
err := errors.New("internal error")
wrapErr := fmt.Errorf("wrap error: %w", err)
if err != nil {
fmt.Println("error:", err)
}
与此同时,Go 语言还引入了 errors 包的 Is、 As 和 Unwrap()函数,用于便捷地检查和操作错误。
看一下 Unwrap()函数,它返回错误链中第一个非 nil 错误:
# src/errors/wrap.go
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
再来看一下 Is 函数,它用于检查错误链中是否存在指定错误:
# src/errors/wrap.go
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
}
}
}
以及 As 函数,它用于提取错误链中的与 target 类型相同的第一个错误,提取成功后,会将错误赋值给 target 指针指向的变量:
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 {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err)
}
return false
}
var errorType = reflectlite.TypeOf((*error)(nil)).Elem()
将 fmt.Errorf,errors.Unwrap,errors.Is 和 errors.As 结合起来,我们可以实现错误处理时追踪堆栈信息的目的。
Go1.20 - Go1.23 从错误链到错误树
在实际应用中,有时候,我们需要同时处理多个错误,比如,校验多个参数是否合法,或者同时读取多个文件内容。我们需要将多个错误信息汇总到一个错误中,方便后续处理。
此版本新增了 errors.Join 函数,用于将多个错误串联起来, 称作错误树:
来看一下 Join 函数的实现:
# src/errors/join.
func Join(errs ...error) error {
n := 0
for _, err := range errs {
if err != nil {
n++
}
}
if n == 0 {
return nil
}
e := &joinError{
errs: make([]error, 0, n),
}
for _, err := range errs {
if err != nil {
e.errs = append(e.errs, err)
}
}
return e
}
joinError 是一个包含多个错误的错误树,实现了 error 接口,并且实现了 Unwrap 函数,返回包装的错误列表:
type joinError struct {
errs []error
}
func (e *joinError) Error() string {
var b []byte
for i, err := range e.errs {
if i > 0 {
b = append(b, '\n')
}
b = append(b, err.Error()...)
}
return string(b)
}
func (e *joinError) Unwrap() []error {
return e.errs
}
同时 errors.Is 和 errors.As 函数也做了更新,支持错误树的处理,与错误链的处理机制保持一致。
fmt.Errorf 也做了更新,支持格式化创建错误树。
err1 := errors.New("internal error1")
err2 := errors.New("internal error2")
wrapErr := fmt.Errorf("wrap error1: %w wrap error2: %w", err1, err2)
if wrapErr != nil {
fmt.Println("error:", wrapErr)
}
需要注意的是,errors.Unwrap 并不能解包错误树,如果指定错误的 Unwarp 返回的是[]error,则返回 nil。(errors.Split 提案被否决)