在过去的十年中,我们成功地利用了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中进行了
激烈的讨论之后,添加了其明确的支持。 在本文中,我们将研究标准库的新增功能,这些功能提供了上述支持:errors包中的三个新功能以及
fmt.Errorf
的新格式命令。
在详细讨论更改之前,让我们谈谈如何在语言的早期版本中研究和构造错误。
Go 1.13之前的错误
错误研究
Go中的错误是含义。 程序以不同的方式根据这些值做出决策。 通常,将错误与nil进行比较,以查看操作是否失败。
if err != nil {
有时我们会比较错误以找出
控制值,并查看是否发生了特定错误。
var ErrNotFound = errors.New("not found") if err == ErrNotFound {
错误值可以是满足该语言定义的错误接口的任何类型。 程序可以使用类型语句或类型开关来查看更特定类型的错误值。
type NotFoundError struct { Name string } func (e *NotFoundError) Error() string { return e.Name + ": not found" } if e, ok := err.(*NotFoundError); ok {
添加信息
函数通常将错误传递给调用堆栈,并向其中添加信息,例如,对错误发生时所发生情况的简短描述。 这很容易做到,只需构造一个新错误,其中包括上一个错误的文本:
if err != nil { return fmt.Errorf("decompress %v: %v", name, err) }
使用
fmt.Errorf
创建新错误时
fmt.Errorf
我们会丢弃除原始错误中的文本以外的所有内容。 正如我们在
QueryError
示例中看到的
QueryError
,有时您需要定义一种包含原始错误的新型错误,以便使用代码将其保存以供分析:
type QueryError struct { Query string Err error }
程序可以查看
*QueryError
并根据原始错误做出决定。 有时称为解开错误。
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
标准库中的
os.PathError
类型是一个错误如何包含另一个错误的另一个示例。
Go 1.13中的错误
开卷方法
在Go 1.13中,
errors
和
fmt
标准库软件包简化
fmt
对包含其他错误的错误的处理。 最重要的是约定,不能更改:包含另一个错误的错误可以实现
Unwrap
方法,该方法返回原始错误。 如果
e1.Unwrap()
返回
e2
,那么我们说
e1
打包了 e2
,您可以
解压缩 e1
以获得
e2
。
根据此约定,可以将上述
QueryError
类型赋予
QueryError
方法,该方法返回其中包含的错误:
func (e *QueryError) Unwrap() error { return e.Err }
解压缩错误的结果可能还包含
Unwrap
方法。 通过反复拆包获得的错误序列,我们称为
错误链 。
Is和As的错误调查
在Go 1.13中,
errors
包包含两个用于调查错误的新功能:
Is
和
As
。
errors.Is
函数将错误与值进行比较。
As
函数检查错误是否为特定类型。
在最简单的情况下,
errors.Is
函数的行为类似于与控制错误的比较,而
errors.As
函数的行为类似于类型声明。 但是,在处理打包错误时,这些函数会评估链中的所有错误。 让我们看一下上面的
QueryError
示例,以检查原始错误:
if e, ok := err.(*QueryError); ok && e.Err == ErrPermission {
使用
errors.Is
函数,
errors.Is
可以这样编写:
if errors.Is(err, ErrPermission) {
errors
软件包还包含一个新的
Unwrap
函数,该函数返回调用错误的
Unwrap
方法的结果,如果错误没有
Unwrap
方法,则返回nil。 通常最好使用
errors.Is
或
errors.As
,因为它们使您可以通过一次调用检查整个链。
包装错误,含%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 {
将错误与
%w
打包在一起可用于
errors.Is
和
errors.As
:
err := fmt.Errorf("access denied: %w", ErrPermission) ... if errors.Is(err, ErrPermission) ...
什么时候收拾?
使用
fmt.Errorf
或自定义类型实现向错误中添加其他上下文时,您需要确定新错误是否将包含原始错误。 没有一个单一的答案,这完全取决于创建新错误的环境。 打包以显示呼叫者。 如果这会导致实现细节的泄露,请不要打包错误。
例如,想象一个
Parse
函数从
io.Reader
读取一个复杂的数据结构。 如果发生错误,我们将要找出发生错误的行和列的编号。 如果从
io.Reader
读取时发生错误,我们将需要打包以找出原因。 由于为调用方提供了
io.Reader
函数,因此有必要显示它生成的错误。
另一种情况:进行多个数据库调用的函数可能不应返回错误,在这些错误中打包了其中一个调用的结果。 如果此功能使用的数据库是实现的一部分,则公开这些错误将违反抽象。 例如,如果
pkg
包中的
LookupUser
函数使用Go
database/sql
包,则它可能会遇到
sql.ErrNoRows
错误。 如果使用
fmt.Errorf("accessing DB: %v", err)
返回错误
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"}) {
errors.As
函数还建议使用
As
方法(如果有)。
错误和包API
返回错误的程序包(大多数程序包都这样做)应该描述程序员可以依赖的这些错误的属性。 一个经过精心设计的程序包也将避免返回带有无法依赖的属性的错误。
最简单的事情是说操作是否成功,分别返回值nil或non-nil。 在许多情况下,不需要其他信息。
如果您需要该函数返回可识别的错误状态,例如“找不到元素”,则可以返回其中打包了信号值的错误。
var ErrNotFound = errors.New("not found")
还有其他模式可提供调用者可以通过语义检查的错误。 例如,直接返回控制值,特定类型或可以使用谓词函数分析的值。
无论如何,请勿向用户透露内部细节。 如“何时值得包装?”一章中所述,如果从另一个软件包返回错误,则将其转换为不显示原始错误,除非您打算将来自己返回该特定错误。
f, err := os.Open(filename) if err != nil {
如果函数返回带有打包信号值或类型的错误,则不要直接返回原始错误。
var ErrPermission = errors.New("permission denied")
结论
尽管我们仅讨论了三个函数和一个格式化命令,但我们希望它们将有助于大大改善Go程序中的错误处理。 我们希望为了提供更多上下文而进行打包将成为一种常规做法,以帮助程序员做出更好的决策并更快地发现错误。
正如Russ Cox
在GopherCon 2019上的
演讲中所说的那样 ,在Go 2的路上,我们进行了实验,简化和发布。 现在,在交付了这些更改之后,我们开始进行新的实验。