Go Wiki:错误值:常见问题
Go 2 错误值提案 为 Go 1.13 的标准库的 errors
和 fmt
软件包添加了功能。对于较早的 Go 版本,还有一个兼容性软件包 golang.org/x/xerrors
。
我们建议使用 xerrors
包以实现向后兼容性。当您不再希望支持 1.13 之前的 Go 版本时,请使用相应的标准库函数。本常见问题解答使用 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)
这仍然会破坏使用 ==
或类型断言来测试错误的客户端。但正如我们在本常见问题解答的第一个问题中讨论的那样,错误的使用者应该迁移到 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 的一部分。