处理Go 1.13中的错误


在过去的十年中,我们成功地利用了Go将错误作为值处理的事实。 尽管标准库对错误的支持最少:仅errors.Newfmt.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 { // 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创建新错误时fmt.Errorf我们会丢弃除原始错误中的文本以外的所有内容。 正如我们在QueryError示例中看到的QueryError ,有时您需要定义一种包含原始错误的新型错误,以便使用代码将其保存以供分析:

 type QueryError struct { Query string Err error } 

程序可以查看*QueryError并根据原始错误做出决定。 有时称为解开错误。

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

标准库中的os.PathError类型是一个错误如何包含另一个错误的另一个示例。

Go 1.13中的错误


开卷方法


在Go 1.13中, errorsfmt标准库软件包简化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包包含两个用于调查错误的新功能: IsAs

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函数, errors.Is可以这样编写:

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

errors软件包还包含一个新的Unwrap函数,该函数返回调用错误的Unwrap方法的结果,如果错误没有Unwrap方法,则返回nil。 通常最好使用errors.Iserrors.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 { // Return an error which unwraps to err. return fmt.Errorf("decompress %v: %w", name, err) } 

将错误与%w打包在一起可用于errors.Iserrors.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"}) { // err's User field is "someuser". } 

errors.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() { 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的路上,我们进行了实验,简化和发布。 现在,在交付了这些更改之后,我们开始进行新的实验。

Source: https://habr.com/ru/post/zh-CN473658/


All Articles