通常来说,有以下两种场景需要对error进行包装.
作者对上面的场景各举了一个例子进行说明。对需要向error中添加上下文信息的情况,以数据库操作为例,某个角色身份的人请求数据库操作,但是它没有查询权限,当它在查询的时候会返回一个没有访问权限的error. 为了方便debug问题,通过log记录错误信息,最好是记录上下文信息。本例中,我们可以通过wrap error记录是谁在访问什么资源的信息。
另一个例子是将error转为一个特定的error,作者以实现HTTP handler为例,该函数需要对调用的函数进行返回值进行检查,如果是对资源没有访问权限的error,将其包装为Forbidden类型的error,然后可以返回403状态码。这种情况下,可以通过wrap error操作将原error包装到Forbidden中。
上面的两个例子中,原始error是能够继续访问到的。调用者caller可以通过unwrap操作就能获取到原始错误error信息。需要注意的是,有时候我们可以将上面两个例子中的方法结合起来使用,即添加上下文信息又将它转换为一个特定的错误。
前面介绍了wrap error使用的两种场景,下面分析了error返回时的4种处理方法。分别是「直接返回、通过自定义error包装返回、通过%w和%v返回」。以下面的代码为例
func Foo() error {
err := bar()
if err != nil {
// ?
}
// ...
}
上面代码中?的地方,有哪些处理方法?
第一种是直接返回,不做任何处理. 这种方法适合没有有用的上下文信息需要添加,也不需要新产生一个error的情况。
if err != nil {
return err
}
第二种是自定义一个error类型返回。在Go1.13之前,wrap error需要先定义一个结构体,实现Error() string
方法。这种通过自定义类型的优点是非常灵活,可以根据需要添加任何额外的信息。缺点是想重复这个操作比较麻烦,必须创建一个特定的错误类型。
type BarError struct {
Err error
}
func (b BarError) Error() string {
return "bar failed:" + b.Err.Error()
}
将原error包装放入BarError中的Err字段中, 代码如下。
if err != nil {
return BarError{Err: err}
}
第三种方法是使用直接%w, 这个是在Go1.13引入的,性质同第二种方法,优点是不用定义一个结构体实现Error方法,并且原始的错误任然是可以访问的。使用者可以通过unwrap error拿到原始的error,然后将原始的error与某种具体的类型或value进行比较。
if err != nil {
return fmt.Errorf("bar failed: %w", err)
}
第四种方法是使用%v,示例代码如下. 与第三种方法的区别在于,使用%v返回的error不是对原error的wrap操作,而是将其转成了另一个error, 原error无法在访问了, 即调用方不能unwrap当前的error将它与原始的bar error进行比较。虽然原error不能访问,但是原error描述的信息是可以获取的。
if err != nil {
return fmt.Errorf("bar failed: %v", err)
}
「因此,%v比%w限制更多,那是不是说在%w发布以后,我们全部使用%w就更好?」 并不完全是这样的。wrap error意味着调用者可以访问原error,这意味着调用方和被调用方存在潜在的耦合。上面的例子中,假如使用wrap error操作,调用Foo
函数检查原error是否是bar error,现在实现有调整,使用其他的函数返回了另一种类型的错误,这将可能会破坏调用方。
「如果我们想让调用方不要依赖于被调用函数的具体实现细节,不应该将error 进行wrap返回,而是直接转换返回,这种情况下,使用%v而不是%w」
下面是上面四种方法的归纳总结,wrap error使用在需要添加上下文信息和将error作为一个具体类型这两种场景中。如果需要标记一个错误,我们需要自定义一个错误类型。如果我们仅仅是添加上下文信息,应该使用fmt.Errorf+%w。如果我们不想让调用者和被调用者存在耦合,不应该使用wrap error而应该直接使用fmt.Errorf+%v.
option | extra context | marking an error(标记一个错误) | source error available |
---|---|---|---|
直接返回error | 不可以 | 不可以 | 可以 |
自定义错误类型 | 可以,如果结构体中包含有承载额外信息的字段 | 可以 | 可以,如果结构体中含有承载原error的字段,并且是可以导出的,或者提供有原error访问方法 |
fmt.Errorf+%w | 可以 | 不可以 | 可以 |
fmt.Errorf+%v | 可以 | 不可以 | 不可以 |