Go 的错误和异常处理 - 4 error

go 1.13 errors

以套娃的方式保存错误链。

  1. 使用 fmt.Errorf%w error 可以包裹着其他 error。
  2. 使用 %+v 打印 error 时,带有堆栈信息,精确到函数名与行号。

用法

// 在 err 的链中找到与目标匹配的第一个错误,如果有则返回 true,否则返回 false
As(err error, target interface{}) bool

// 判断两个 error 是否相等
Is(err error, target error) bool

// 返回一个新的 error 对象,即使内容一样也是两个不同的对象
New(text string) error

// 如果传入的 err 对象中有 %w 关键字的格式化类容,则会在返回值中解析出这个原始 error,多层嵌套只返回第一个,否则返回 nil
Unwarp(err error) error

创建

方法 1 fmt.Errorf

err1 := errors.New("new error")
err2 := fmt.Errorf("err2 : [%w]", err1)
err3 := fmt.Errorf("err3 : [%w]", err2)
fmt.Println(err3)

方法 2 自定义 struct

type WarpError struct {
  msg string
  err error
}

func (e *WarpError) Error() string {
  return e.msg
}

func (e *WarpError) Unwrap() string {
  return e.err
}

拆开

err1 := errors.New("new error")
err2 := fmt.Errorf("err2 : [%w]", err1)
err3 := fmt.Errorf("err3 : [%w]", err2)

fmt.Println(errors.Unwrap(errors.Unwrap(err3)))

错误判断

errors.Is

以前只有一个错误,现在是错误链表。可能一个 err 会 wrapping 了其它 error,因为不知道嵌套了几次,所以现在要通过 errors.Is 遍历判断。

它递归调用 Unwrap 并判断每一层的 err 是否相等,如果有任何一层 err 和传入的目标错误相等,则返回 true。

err1 := errors.New("new error")
err2 := fmt.Errorf("err2 : [%w]", err1)
err3 := fmt.Errorf("err3 : [%w]", err2)

// 过去写法
// if err3 == os.ErrNotExistF {
// }

fmt.Println(errors.Is(err3, err1)) // true

errors.As

之前没有 wrapping error 的时候,我们要把 error 转为另外一个 error,一般都是使用 type assertion 或者 type switch 其实也就是类型断言。

if perr, ok := err.(*os.PathError); ok {
    fmt.Println(perr.Path)
}

但是现在给你返回的 err 可能是已经被嵌套了,甚至好几层了,这种方式就不能用了。

与 Is 的区别是,Is 是严格判断相等,As 判断类型是否相同,并提取第一个符合目标类型的错误,用来统一处理某一类错误。

type ErrorString struct {
  s string
}

func (e *ErrorString) Error() string {
  return e.s
}

var targetErr *ErrorString
err := fmt.Errorf("new error: [%w]", &ErrorString{s: "target err"})
fmt.Println(errors.As(err, &target)) // true

Error Linter

golangci-lint 有两个跟 error 有关的 wrapcheckgo-errorlint

warpcheck

warpcheck 帮助我们确保不会忘记给 error 添加额外的信息。作者认为 github.com/pkg/errors 会因为调用 runtime.Stack(buf []byte, all bool) int 引起性能问题;打印堆栈只会告诉我们发生错误的位置,并不能说明原因;另外多次包装带堆栈的错误信息,最终会在日志中输入大量的日志。

因此在错误中添加额外的上下文仍然是很好的方案。

  1. 只要你认为在阅读日志时候,对开发人员有帮助,就可以添加额外上下文。
  2. 从其它 package 返回的错误,添加上下文可以很方便的确定入口点。

选用的 error 类库还是 golang.org/pkg/errors

func (db *DB) getTansactionByID(tranID string) (Transaction, error) {
  sql := `SELECT * FROM transaction WHERE id = $1;`

  var t Transaction
  if err := db.conn.Get(&t, sql, tranID); err != nil {
    return Transaction{}, fmt.Errorf("failed to get transaction with ID %s: %v", tranID, err)
  }

  return t, nil
}

相关阅读 Introducing Wrapcheck: An error wrapping linter for Go

go-errorlint

go-errorlint 帮我们处理下面的疏忽。

// bad
fmt.Errorf("oh noes: %v", err)
// ^ non-wrapping format verb for fmt.Errorf. Use `%w` to format errors

// good
fmt.Errorf("oh noes: %w", err)

// bad
err == ErrFoo
// ^ comparing with == will fail on wrapped errors. Use errors.Is to check for a specific error

// good
errors.Is(err, ErrFoo)

// bad
myErr, ok := err.(*MyError)
// ^ type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors

// good
var me MyError
ok := errors.As(err, &me)

相关链接

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @Victor Sep 7, 2020

奉献爱心