Go Wiki:错误值:常见问题解答
Go 2 错误值提案 为 Go 1.13 标准库的 errors
和 fmt
包添加了功能。还有一个兼容性包 golang.org/x/xerrors
,用于早期 Go 版本。
我们建议使用 xerrors
包以获得向后兼容性。当您不再希望支持 1.13 之前的 Go 版本时,请使用相应的标准库函数。本 FAQ 使用 Go 1.13 中的 errors
和 fmt
包。
我应该如何修改我的错误处理代码以适应新功能?
您需要做好准备,您可能获得的错误是包装过的。
-
如果您当前使用
==
比较错误,请改用errors.Is
。示例if err == io.ErrUnexpectedEOF
变为
if errors.Is(err, io.ErrUnexpectedEOF)
- 形式为
if err != nil
的检查无需更改。 - 与
io.EOF
的比较无需更改,因为io.EOF
永远不应该被包装。
- 形式为
-
如果您使用类型断言或类型开关检查错误类型,请改用
errors.As
。示例if e, ok := err.(*os.PathError); ok
变为
var e *os.PathError if errors.As(err, &e)
- 使用此模式还可以检查错误是否实现了某个接口。(这是少数几种接口指针合适的情况之一。)
- 将类型开关重写为一系列 if-else。
我已经在用 fmt.Errorf
配合 %v
或 %s
为错误提供上下文。我什么时候应该切换到 %w
?
通常会看到类似这样的代码:
if err := frob(thing); err != nil {
return fmt.Errorf("while frobbing: %v", err)
}
有了新的错误功能,这些代码将与以前完全一样工作,生成一个包含 err
文本的字符串。从 %v
更改为 %w
不会改变该字符串,但它会包装 err
,允许调用者使用 errors.Unwrap
、errors.Is
或 errors.As
来访问它。
所以,如果您想向调用者公开底层错误,请使用 %w
。请记住,这样做可能会暴露实现细节,从而限制您代码的演进。调用者可以依赖您正在包装的错误的类型和值,因此更改该错误现在可能会破坏他们。例如,如果您的包 pkg
的 AccessDatabase
函数使用 Go 的 database/sql
包,那么它可能会遇到 sql.ErrTxDone
错误。如果您返回该错误并使用 fmt.Errorf("accessing DB: %v", err)
,那么调用者将看不到 sql.ErrTxtDone
是您返回的错误的一部分。但如果您改为返回 fmt.Errorf("accessing DB: %w", err)
,那么调用者可以合理地写:
err := pkg.AccessDatabase(...)
if errors.Is(err, sql.ErrTxDone) ...
此时,如果您不想破坏您的客户,您必须始终返回 sql.ErrTxDone
,即使您切换到不同的数据库包。
如何在不破坏现有客户端的情况下为我已返回的错误添加上下文?
假设您的代码现在看起来像这样:
return err
然后您决定在返回 err
之前为它添加更多信息。如果您写:
return fmt.Errorf("more info: %v", err)
那么您可能会破坏您的客户,因为 err
的身份丢失了;只剩下它的消息。
您可以通过使用 %w
来包装错误,编写:
return fmt.Errorf("more info: %w", err)
这仍然会破坏使用 ==
或类型断言来测试错误的客户。但正如我们在本 FAQ 的第一个问题中所讨论的,错误的消费者应该迁移到 errors.Is
和 errors.As
函数。如果您能确保您的客户已经这样做了,那么从...切换不是一个破坏性更改:
return err
转换为
return fmt.Errorf("more info: %w", err)
我正在编写新代码,没有客户端。我应该包装返回的错误还是不包装?
由于您没有客户,您不受向后兼容性的约束。但您仍然需要权衡两个相互对立的考虑:
- 让客户端代码访问底层错误可以帮助它做出决策,从而带来更好的软件。
- 您公开的每个错误都将成为您 API 的一部分:您的客户可能会依赖它,因此您无法更改它。
对于您返回的每个错误,您都必须权衡帮助您的客户和锁定自己的选择。当然,这个选择不仅仅是错误;作为包的作者,您会做出许多决定,例如您的代码的某个功能对客户来说是否重要,还是实现细节。
但是,对于错误,有一个中间选择:您可以将错误细节公开给阅读您代码错误消息的人,而无需将错误本身暴露给客户端代码。一种方法是使用 fmt.Errorf
和 %s
或 %v
将细节放入字符串中。另一种方法是编写一个自定义错误类型,将详细信息添加到其 Error
方法返回的字符串中,并避免定义 Unwrap
方法。
我维护一个导出错误检查谓词函数的包。我应该如何适应新功能?
您的包有一个函数或方法 IsX(error) bool
,它报告一个错误是否具有某个属性。一个自然的思路是修改 IsX
来解包它所传入的错误,检查包装错误链中每个错误的属性。我们不建议这样做:行为的改变可能会破坏您的用户。
您的情况与标准 os
包类似,该包有几个这样的函数。我们推荐我们采取的方法。os
包有几个谓词,但我们大部分都像对待它们一样。为具体起见,我们将看看 os.IsExist
。
我们没有更改 os.IsExist
,而是让 errors.Is(err, os.ErrExist)
的行为与它类似,不同之处在于 Is
会解包。 (我们通过让 syscall.Errno
实现一个 Is
方法来做到这一点,如 errors.Is
的文档中所述。)使用 errors.Is
将始终正确工作,因为它只存在于 Go 1.13 及更高版本中。对于旧版本的 Go,您应该自己递归解包错误,对每个底层错误调用 os.IsExist
。
此技术仅在您能够控制被包装的错误时才有效,这样您就可以向它们添加 Is
方法。在这种情况下,我们建议:
- 不要更改您的
IsX(error) bool
函数;但要更改其文档,以说明它不解包。 - 如果您还没有一个,请添加一个类型实现
error
的全局变量,该变量表示您的函数测试的条件。var ErrX = errors.New("has property X")
- 向
IsX
返回 true 的类型添加一个Is
方法。当其参数等于ErrX
时,Is
方法应返回 true。
如果您无法控制所有具有属性 X 的错误,您应该考虑添加另一个函数,该函数在解包时测试该属性,例如:
func IsXUnwrap(err error) bool {
for e := err; e != nil; e = errors.Unwrap(e) {
if IsX(e) {
return true
}
}
return false
}
或者您可以保持现状,让您的用户自己解包。无论哪种方式,您仍然应该更改 IsX
的文档,以说明它不解包。
我有一个实现了 error
并包含嵌套错误的类型。我应该如何将其适应新功能?
如果您的类型已经公开了错误,请编写一个 Unwrap
方法。
例如,您的类型可能看起来像:
type MyError struct {
Err error
// other fields
}
func (e *MyError) Error() string { return ... }
然后您应该添加:
func (e *MyError) Unwrap() error { return e.Err }
这样,您的类型将与 errors
和 xerrors
的 Is
和 As
函数正确配合使用。
我们已经为标准库中的 os.PathError
和其他类似类型这样做了。
很明显,如果嵌套错误是导出的,或者通过 Unwrap
等方法对您的包外部代码可见,那么编写 Unwrap
方法是正确的选择。但是,如果嵌套错误没有暴露给外部代码,您最好保持原样。通过从 Unwrap
返回错误来使错误可见,将使您的客户能够依赖嵌套错误的类型,这可能会暴露实现细节并限制您包的演进。有关更多信息,请参见上面关于 %w
的讨论。
此内容是 Go Wiki 的一部分。