Go Error 的处理最佳实践

Golang
350
0
0
2022-11-22

Go 的 error 和 Java 的 Exception 最明显的区别在于:

  • 原生库不携带 stacktrace
  • 原生库不支持 Wrap

这给程序 debug 带来了一些麻烦,因此我们会使用 github.com/pkg/errors 来替代原生 errors 包来处理 Error。

但又因第三方库的 error 大概率没有使用 github.com/pkg/errors,处理方式不一致会造成麻烦,下面定义一套规则来统一:

  • 自己 new 的 error,根据情况包含 stacktrace
  • 不要 wrap 自己代码返回的 error
  • wrap 第三方库返回的 error
  • 尽量只把 error 用作异常情况

下面详细解说。

自己 new 的 error,根据情况包含 stacktrace

如果把 error 当作一种返回值,那么这种情况下不需要 stacktrace,比如:

import "errors"

// 关闭订单
func closeOrder(id string) error {
    ...
    if order.IsPaid {
        return errors.New("不允许关闭已支付订单")
    }
    ...
}

如果把 error 当作一种异常结果,那么就要携带 stacktrace:

import "github.com/pkg/errors"

// 关闭订单
func closeOrder(id string) error {
    ...
    if dbDown {
        // github.com/pkg/errors New 函数会携带 stacktrace 信息 
        return errors.New("数据库宕机")
    }
    ...
}

当然这个 error 到底是返回值,还是异常结果完全是你自己决定的。

不要 wrap 自己代码返回的 error

wrap error 的目的是给 error 包上 stacktrace。

而当你调用自己写的代码时,被调代码自身就已经决定了是否携带 stacktrace(见前一条),那么在这里就不用再 wrap 了。

// 关闭订单
func closeOrder(id string) error {
    ...
}

// 批量关闭订单
func batchCloseOrder(ids []string) error {
    for _, id := range ids {
        err := closeOrder(id)
        if err != nil {
            // 这里不需要 wrap,直接上抛 
            return err
        }
    }
}

wrap 第三方库返回的 error

第三方库的代码绝大多数都没有使用 github.com/pkg/errors,而且它们的设计基本上是把 error 作为异常情况的。所以遇到这种情况,你应该 wrap 之后再上抛:

import "github.com/pkg/errors"

_, err := db.ExecContext(ctx, query, ...)
if err != nil {
    // github.com/pkg/errors Wrap 函数会包上 stacktrace 
    return errors.Wrap(err, "update Order failed")
}
// 如果简单点也可以这样
return errors.WithStack(err)

尽量只把 error 用作异常情况

Error handling and Go 中提到:

Go code uses error values to indicate an abnormal state

所以尽量只把 error 用作异常情况,而不是一种返回值。

打印 error 的 stacktrace

errors 构造的 error 和大多数第三方库返回的 error 不携带 stacktrace,所以是打印不出来的:

import "errors"

fmt.Print(errors.New("abc"))
// 结果
// abc

fmt.Printf("%v", errors.New("abc"))
// 结果
// abc

使用 github.com/pkg/errors 构造的 error 则需要特殊的方式才能打印出 stacktrace:

import "github.com/pkg/errors"

fmt.Print(errors.New("abc"))
// 结果
// abc

fmt.Printf("%v", errors.New("abc"))
// 结果
// abc

fmt.Printf("%+v", errors.New("abc"))
// 结果
// abc
// somefunc
// 	/path/to/some_func.go:<line>
// testing.tRunner
// 	/usr/local/opt/go/libexec/src/testing/testing.go:1259
// runtime.goexit
// 	/usr/local/opt/go/libexec/src/runtime/asm_amd64.s:1581

可以看到使用 fmt format %+v 的时候才会打印出 stacktrace。

某些日志库,比如 go.uber.org/zap自动识别来自 github.com/pkg/errors 的 error,会打印 stacktrace(注意 errVerbose 字段):

import (
    "github.com/pkg/errors" 
    "go.uber.org/zap"
)

logger.Errorw("errors.New", "err", errors.New("simple"))
// 结果
// {"level":"error","ts":"...","caller":"...","msg":"errors.New","err":"simple","errVerbose":"<stacktrace>"}

logger.Errorw("errors.New", zap.NamedError("err", errors.New("simple")))
// 结果和上面一样