Go Wiki:错误值:常见问题解答

Go 2 错误值提案 为 Go 1.13 标准库的 errorsfmt 包添加了功能。还有一个兼容性包 golang.org/x/xerrors,用于早期 Go 版本。

我们建议使用 xerrors 包以获得向后兼容性。当您不再希望支持 1.13 之前的 Go 版本时,请使用相应的标准库函数。本 FAQ 使用 Go 1.13 中的 errorsfmt 包。

我应该如何修改我的错误处理代码以适应新功能?

您需要做好准备,您可能获得的错误是包装过的。

  • 如果您当前使用 == 比较错误,请改用 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.Unwraperrors.Iserrors.As 来访问它。

所以,如果您想向调用者公开底层错误,请使用 %w。请记住,这样做可能会暴露实现细节,从而限制您代码的演进。调用者可以依赖您正在包装的错误的类型和值,因此更改该错误现在可能会破坏他们。例如,如果您的包 pkgAccessDatabase 函数使用 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.Iserrors.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 }

这样,您的类型将与 errorsxerrorsIsAs 函数正确配合使用。

我们已经为标准库中的 os.PathError 和其他类似类型这样做了。

很明显,如果嵌套错误是导出的,或者通过 Unwrap 等方法对您的包外部代码可见,那么编写 Unwrap 方法是正确的选择。但是,如果嵌套错误没有暴露给外部代码,您最好保持原样。通过从 Unwrap 返回错误来使错误可见,将使您的客户能够依赖嵌套错误的类型,这可能会暴露实现细节并限制您包的演进。有关更多信息,请参见上面关于 %w 的讨论。


此内容是 Go Wiki 的一部分。