错误值类型检视策略

错误值类型检视策略

错误值类型检视策略 #

哨兵错误处理策略,除了让错误处理方可以“有的放矢”的进行值比较之外,并没有提供其他有效的错误上下文信息。那如果遇到错误处理方需要错误值提供更多的“错误上下文”的情况,上面这些错误处理策略和错误值构造方式都无法满足。

这种情况下,我们需要通过自定义错误类型的构造错误值的方式,来提供更多的“错误上下文”信息。并且,由于错误值都通过 error 接口变量统一呈现,要得到底层错误类型携带的错误上下文信息,错误处理方需要使用 Go 提供的类型断言机制(Type Assertion)或类型选择机制(Type Switch),这种错误处理方式,我称之为错误值类型检视策略。

我们来看一个标准库中的例子加深下理解,这个 json 包中自定义了一个 UnmarshalTypeError的错误类型:

// $GOROOT/src/encoding/json/decode.go
type UnmarshalTypeError struct {
    Value  string
    Type   reflect.Type
    Offset int64
    Struct string
    Field  string
}

错误处理方可以通过错误类型检视策略,获得更多错误值的错误上下文信息,下面就是利用这一策略的 json 包的一个方法的实现:

// $GOROOT/src/encoding/json/decode.go
func (d *decodeState) addErrorContext(err error) error {
    if d.errorContext.Struct != nil || len(d.errorContext.FieldStack) > 0 {
        switch err := err.(type) {
        case *UnmarshalTypeError:
            err.Struct = d.errorContext.Struct.Name()
            err.Field = strings.Join(d.errorContext.FieldStack, ".")
            return err
        }
    }
    return err
}

我们看到,这段代码通过类型 switch 语句得到了 err 变量代表的动态类型和值,然后在匹配的 case 分支中利用错误上下文信息进行处理。

这里,一般自定义导出的错误类型以XXXError的形式命名。和“哨兵”错误处理策略一样,错误值类型检视策略,由于暴露了自定义的错误类型给错误处理方,因此这些错误类型也和包的公共函数 / 方法一起,成为了 API 的一部分。一旦发布出去,开发者就要对它们进行很好的维护。而它们也让使用这些类型进行检视的错误处理方对其产生了依赖。

从 Go 1.13 版本开始,标准库 errors 包提供了As函数给错误处理方检视错误值。As函数类似于通过类型断言判断一个 error 类型变量是否为特定的自定义错误类型,如下面代码所示:

// 类似 if e, ok := err.(*MyError); ok { … }
var e *MyError
if errors.As(err, &e) {
    // 如果err类型为*MyError,变量e将被设置为对应的错误值
}

不同的是,如果 error 类型变量的动态错误值是一个包装错误,errors.As函数会沿着该包装错误所在错误链,与链上所有被包装的错误的类型进行比较,直至找到一个匹配的错误类型,就像 errors.Is 函数那样。下面是As函数应用的一个例子:

type MyError struct {
    e string
}
func (e *MyError) Error() string {
    return e.e
}
func main() {
    var err = &MyError{"MyError error demo"}
    err1 := fmt.Errorf("wrap err: %w", err)
    err2 := fmt.Errorf("wrap err1: %w", err1)
    var e *MyError
    if errors.As(err2, &e) {
        println("MyError is on the chain of err2")
        println(e == err)
        return
    }
    println("MyError is not on the chain of err2")
}

运行上述代码会得到:

MyError is on the chain of err2
true

我们看到,errors.As函数沿着 err2 所在错误链向下找到了被包装到最深处的错误值,并将 err2 与其类型 * MyError成功匹配。匹配成功后,errors.As 会将匹配到的错误值存储到 As 函数的第二个参数中,这也是为什么println(e == err)输出 true 的原因。

Viewpoint #

From #

22|函数:怎么结合多返回值进行错误处理?