Golang 错误处理:提供对用户友好的错误对象

53 阅读3分钟

Golang 程序中,函数执行出错的标准做法是返回一个实现了 error 接口的对象。函数调用者遇到返回值中的 error 对象非空时,大部分情况下会直接将这个对象返回给上一级的调用者。即使在最外层的 main 函数,也可以抛一个 panic 把问题甩给用户。或者在 Web 服务的 API 层,返回一个 500。我们不生产错误,我们只是大自然的搬运工。😛

作为一个负责任的开发者,能处理的错误还是应该尽早处理,这时我们就需要知道究竟出了什么错。标准的 error 对象只提供了获取错误消息字符串的接口,通过字符串的内容判断出了什么错,不是一个鲁棒的做法。一些实现友好的库会为每种错误类型定义一个错误对象,比如 sql.ErrNoRows,调用方可以通过判断错误对象的指针和库中定义的哪个错误对象相等,来判断具体的错误类型。然而,从错误发生到当前位置可能经过了不止一层函数调用,中间某层可能会为了提供更充分的错误现场信息,通过 fmt.Errorf 或其他方式构造出新的 error 对象。这时就无法通过直接比对指针来判断具体的错误类型了,因为新构造的 error 对象不会与任何一个预定义的错误对象相等。

Golang 1.13 版本开始,标准库中提供了新的错误处理机制fmt.Errorf 会创建一个实现了 Unwrap() 函数的 error 对象,Unwrap() 函数会返回被封装起来的原始 error 对象。为了避免每次都一层一层调用 Unwrap() 函数,标准库中还提供了 errors.Is(err, target error) 函数,这个函数会像剥洋葱一样,通过调用 Unwrap() 把封装起来的 err 一层层提取出来,依次判断和 target 是否相等。

现在还有一个问题,预定义的错误对象创建好之后就不能再更新了,没法在错误信息中加入更具体的信息。加入更具体的信息需要创建新的对象,创建了新的对象就不能再直接与预定义的错误对象判断是否相等了。标准库中其实也提供了解决方案,errors.Is 中会判断 err 是否实现了 Is(target error) bool 这个函数,并尝试用 target 作为参数调用这个函数。如果函数返回 true,那么 errors.Is 就会返回 true。 不过官方文档中并没有提供具体如何利用这个机制的例子,那么我来写一个吧。

var ErrSomethingFailed = errors.New("something failed")

假设我们定义了 ErrSomethingFailed 这个对象来表示发生了某种错误。当我们想返回一个包含更具体的错误信息的对象时,可以定义下面这样一个结构体:

type errorSomethingFailedWithReason struct {
    reason string
}

func NewErrorSomethingFailedWithReason(reason string) error {
    return &errorSomethingFailedWithReason{reason}
}

func (e *errorSomethingFailedWithReason) Error() string {
    return fmt.Sprintf("something failed: %s", e.reason)
}

func (e *errorSomethingFailedWithReason) Is(target error) bool {
    if ErrSomethingFailed == target {
        return true
    }
    _, ok := target.(errorSomethingFailedWithReason)
    return ok
}

当发生错误时,可以调用 NewErrorSomethingFailedWithReason 来创建一个包含具体错误信息的错误对象 err,并返回给上层调用方。调用方拿到这个错误对象,调用 errors.Is(err, ErrSomethingFailed) 时也会返回 true,虽然 errErrSomethingFailed 并不相等,甚至连它们的实际结构体都不一样。