Skip to main content

Error

在最简单的情况下,errors.Is函数的行为类似于上面对哨兵错误(sentinel error))的比较,而errors.As函数的行为类似于类型断言(type assertion)。但是,在处理包装错误(包含其他错误的错误)时,这些函数会考虑错误链中的所有错误。让我们再次看一下通过展开QueryError以检查潜在错误:

if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}

使用errors.Is函数,我们可以这样写:

if errors.Is(err, ErrPermission) {
// err, or some error that it wraps, is a permission problem
}

用%w包装错误

如前面所述,我们通常使用fmt.Errorf函数向错误添加其他信息。

if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}

在Go 1.13中,fmt.Errorf函数支持新的%w动词。当存在该动词时,所返回的错误fmt.Errorf将具有Unwrap方法,该方法返回参数%w对应的错误。%w对应的参数必须是错误(类型)。在所有其他方面,%w与%v等同。

if err != nil {
// Return an error which unwraps to err.
return fmt.Errorf("decompress %v: %w", name, err)
}

使用%w创建的包装错误可用于errors.Is和errors.As:

err := fmt.Errorf("access denied: %w”, ErrPermission)
...
if errors.Is(err, ErrPermission) …

### 使用Is和As方法自定义错误测试

errors.Is函数检查错误链中的每个错误是否与目标值匹配。默认情况下,如果两者相等,则错误与目标匹配。另外,链中的错误可能会通过实现Is方法来声明它与目标匹配。

例如,下面的错误类型定义是受[Upspin error包](https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html)的启发,它将错误与模板进行了比较,并且仅考虑模板中非零的字段:


type Error struct {
Path string
User string
}

func (e *Error) Is(target error) bool {
t, ok := target.(*Error)
if !ok {
return false
}
return (e.Path == t.Path || t.Path == "") &&
(e.User == t.User || t.User == "")
}

if errors.Is(err, &Error{User: "someuser"}) {
// err's User field is "someuser".
}

同样,errors.As函数将使用链中某个错误的As方法,如果该错误实现了As方法。

错误和包API

返回错误的程序包(大多数都会返回错误)应描述程序员可能依赖的那些错误的属性。一个经过精心设计的程序包也将避免返回带有不应依赖的属性的错误。

最简单的规约是用于说明操作成功或失败的属性,分别返回nil或non-nil错误值。在许多情况下,不需要进一步的信息了。

如果我们希望函数返回可识别的错误条件,例如“item not found”,则可能会返回包装哨兵的错误。

var ErrNotFound = errors.New("not found")

// FetchItem returns the named item.
//
// If no item with the name exists, FetchItem returns an error
// wrapping ErrNotFound.
func FetchItem(name string) (*Item, error) {
if itemNotFound(name) {
return nil, fmt.Errorf("%q: %w", name, ErrNotFound)
}
// ...
}

还有其他现有的提供错误的模式,可以由调用方进行语义检查,例如直接返回哨兵值,特定类型或可以使用谓词函数检查的值。

在所有情况下,都应注意不要向用户公开内部细节。正如我们在上面的“是否要包装”中提到的那样,当您从另一个包中返回错误时,应该将错误转换为不暴露基本错误的形式,除非您愿意将来再返回该特定错误。

f, err := os.Open(filename)
if err != nil {
// The *os.PathError returned by os.Open is an internal detail.
// To avoid exposing it to the caller, repackage it as a new
// error with the same text. We use the %v formatting verb, since
// %w would permit the caller to unwrap the original *os.PathError.
return fmt.Errorf("%v", err)
}

如果将函数定义为返回包装某些标记或类型的错误,请不要直接返回基础错误。

var ErrPermission = errors.New("permission denied")

// DoSomething returns an error wrapping ErrPermission if the user
// does not have permission to do something.
func DoSomething() {
if !userHasPermission() {
// If we return ErrPermission directly, callers might come
// to depend on the exact error value, writing code like this:
//
// if err := pkg.DoSomething(); err == pkg.ErrPermission { … }
//
// This will cause problems if we want to add additional
// context to the error in the future. To avoid this, we
// return an error wrapping the sentinel so that users must
// always unwrap it:
//
// if err := pkg.DoSomething(); errors.Is(err, pkg.ErrPermission) { ... }
return fmt.Errorf("%w", ErrPermission)
}
// ...
}

结论

尽管我们讨论的更改仅包含三个函数和一个格式化动词(%w),但我们希望它们能大幅改善Go程序中错误处理的方式。我们希望通过包装来提供其他上下文的方式得到Gopher们地普遍使用,从而帮助程序做出更好的决策,并帮助程序员更快地发现错误。