Working with Errors in Go 1.13
Damien Neil and Jonathan Amsterdam
17 October 2019
介绍
在过去的十年中,Go将处理错误作为价值,已为我们服务良好。尽管标准库对错误的支持很简单(仅是errors.New和fmt.Errorf函数,它们产生的错误仅包含一条消息),但是内置的错误接口使 Go 程序员可以添加所需的任何信息。它所需要的只是一种实现Error方法的类型:
type QueryError struct {
Query string
Err error
}
func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() }
像这样的错误类型是普遍存在的,它们存储的信息,差异很大,从时间戳到文件,再到服务器,通常,该信息包括另一个较低级别的错误用来提供其他上下文。
在Go代码中,一个包含另一个错误的错误模式非常普遍,以至于经过广泛讨论,Go 1.13为其添加了明确的支持。这篇文章描述了标准库提供的支持:错误包中的三个新功能,以及fmt.Errorf的新格式动词。
在详细描述更改之前,让我们回顾一下在语言的早期版本中如何检查和构造错误。
Go 1.13之前的错误
检查错误
Go的错误是值,程序通过几种方式根据这些值作出决策,最常见的是将错误与nil进行比较,以查看操作是否失败。
if err != nil {
// something went wrong
}
有时我们将错误与已知的标记值进行比较,以查看是否发生了特定错误。
var ErrNotFound = errors.New("not found")
if err == ErrNotFound {
// something wasn't found
}
错误值可以是满足语言定义的错误接口的任何类型。程序可以使用类型断言或类型开关将错误值视为更特定的类型。
type NotFoundError struct {
Name string
}
func (e *NotFoundError) Error() string { return e.Name + ": not found" }
if e, ok := err.(*NotFoundError); ok {
// e.Name wasn't found
}
添加信息
函数通常在向其添加信息时将错误传递给调用堆栈,例如对错误发生时所发生情况的简要描述。一种简单的方法是构造一个新错误,其中包括上一个错误:
if err != nil {
return fmt.Errorf("decompress %v: %v", name, err)
}
使用fmt.Errorf创建新错误会丢弃原始错误中的所有内容(文本除外)。正如我们在QueryError上看到的那样,有时我们可能想要定义一个包含基础错误的新错误类型,并将其保留以供代码检查。再次是QueryError:
type QueryError struct {
Query string
Err error
}
程序可以查看*QueryError值,以根据潜在错误做出决策。有时您会看到称为“展开”错误的信息。
标准库中的os.PathError类型是一个错误的另一个示例,其中包含另一个错误。
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
// query failed because of a permission problem
}
Errors in Go 1.13
The Unwrap method
Go 1.13为错误和fmt标准库程序包引入了新功能,以简化处理包含其他错误的错误的过程。其中最重要的是约定,而不是更改:包含另一个错误的错误可能实现了Unwrap方法,该方法返回了基础错误。如果e1.Unwrap()返回e2,则说e1包装了e2,您可以将e1拆开以得到e2。
遵循此约定,我们可以将QueryError类型提供给Unwrap方法上方,该方法返回其包含的错误:
func (e *QueryError) Unwrap() error { return e.Err }
解包错误的结果本身可能具有Unwrap方法。我们称重复解开错误链产生的错误序列。
使用Is和As检查错误
Go 1.13错误程序包包括两个用于检查错误的新功能:Is和As。
errors.is函数将错误与值进行比较。
// Similar to:
// if err == ErrNotFound { … }
if errors.Is(err, ErrNotFound) {
// something wasn't found
}
As函数测试错误是否是特定类型。
// Similar to:
// if e, ok := err.(*QueryError); ok { … }
var e *QueryError
if errors.As(err, &e) {
// err is a *QueryError, and e is set to the error's value
}
在最简单的情况下,errors.Is函数的行为类似于对哨兵错误的比较,而errors.As函数的行为类似于类型声明。但是,在处理包装错误时,这些功能会考虑链中的所有错误。让我们从上方再次查看解开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
}
errors程序包还包括一个新的Unwrap函数,该函数返回调用错误的Unwrap方法的结果;如果错误没有Unwrap方法,则返回nil。通常,最好使用error.is或errors.As,因为这些函数将在单个调用中检查整个链。
Wrapping errors with %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与%v相同。
if err != nil {
// Return an error which unwraps to err.
return fmt.Errorf("decompress %v: %w", name, err)
}
用%w包裹一个错误使它可用于error.Is和errors.As:
err := fmt.Errorf("access denied: %w", ErrPermission)
...
if errors.Is(err, ErrPermission) ...
Whether to Wrap
使用fmt.Errorf或通过实现自定义类型向错误添加其他上下文时,您需要确定新错误是否应该包装原始错误。这个问题没有一个答案。它取决于创建新错误的上下文。包装错误以将其公开给调用者。这样做时请不要包装错误,以免暴露实现细节。
举一个例子,假设一个Parse函数从io.Reader读取一个复杂的数据结构。如果发生错误,我们希望报告发生错误的行号和列号。如果从io.Reader读取时发生错误,我们将要包装该错误以检查潜在问题。由于调用者向函数提供了io.Reader,因此暴露由它产生的错误是有意义的。
相反,对数据库进行多次调用的函数可能不应返回将这些调用之一的结果解开的错误。如果该函数使用的数据库是实现细节,那么暴露这些错误就是对抽象的违反。例如,如果软件包pkg的LookupUser函数使用Go的数据库/ sql软件包,则它可能会遇到sql.ErrNoRows错误。如果使用fmt.Errorf(“ accessing DB:%v”,err)返回该错误,则调用者无法在内部查找sql.ErrNoRows。但是,如果该函数返回fmt.Errorf(“ accessing DB:%w”,err),则调用者可以合理地编写
err := pkg.LookupUser(...)
if errors.Is(err, sql.ErrNoRows) …
此时,即使您不希望中断客户端,即使切换到其他数据库包,该函数也必须始终返回sql.ErrNoRows。换句话说,包装错误会使该错误成为您API的一部分。如果您不想将来将错误作为API的一部分来支持,则不应包装该错误。
请务必记住,无论是否换行,错误文本都将相同。试图理解该错误的人将以两种方式获得相同的信息。包装的选择是关于是否给程序提供更多信息,以便他们可以做出更明智的决定,还是保留该信息以保留抽象层。
使用Is和As方法自定义错误测试
errors.is函数检查链中的每个错误是否与目标值匹配。默认情况下,如果两者相等,则错误与目标匹配。此外,链中的错误可能会通过实现Is方法来声明它与目标匹配。
例如,考虑此错误是受Upspin错误包的启发,该错误包将错误与模板进行比较,仅考虑模板中非零的字段:
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".
}
如果存在错误,则As函数会类似地查询As方法。
错误和包API
返回错误的程序包(大多数都会返回错误)应描述 程序员 可能依赖的那些错误的属性。一个经过精心设计的程序包也将避免返回带有不应依赖的属性的错误。
最简单的规范是说操作成功或失败,分别返回nil或non-nil错误值。在许多情况下,不需要进一步的信息。
如果我们希望函数返回可识别的错误条件,例如“未找到项目”,则可能会返回包装哨兵的错误。
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() error {
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)
}
// ...
}
结论
尽管我们讨论的更改仅包含三个功能和一个格式化动词,但我们希望它们对改善Go程序中错误处理的方式有很大帮助。我们希望通过包装来提供其他上下文将变得司空见惯,从而帮助程序做出更好的决策,并帮助程序员更快地发现错误。
正如Russ Cox在GopherCon 2019主题演讲中所说的那样,在Go 2的道路上,我们进行了实验,简化和发布。现在,我们已经发布了这些更改,我们期待接下来的实验。