Go Wiki:错误值:常见问题

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

我们建议使用 xerrors 包以实现向后兼容性。当您不再希望支持 1.13 之前的 Go 版本时,请使用相应的标准库函数。本常见问题解答使用 Go 1.13 中的 errorsfmt 包。

我应该如何更改错误处理代码以使用新功能?

您需要做好准备,因为您收到的错误可能会被包装。

我已经使用 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)

这仍然会破坏使用 == 或类型断言来测试错误的客户端。但正如我们在本常见问题解答的第一个问题中讨论的那样,错误的使用者应该迁移到 errors.Iserrors.As 函数。如果您能确定您的客户端已经这样做,那么从

return err

切换到

return fmt.Errorf("more info: %w", err)

我正在编写没有客户端的新代码。我应该包装返回的错误吗?

由于您没有客户端,因此您不受向后兼容性的约束。但您仍然需要平衡两个对立的考虑因素

对于您返回的每个错误,您必须权衡帮助您的客户端和锁定您自己的选择。当然,这种选择并不仅限于错误;作为包作者,您会做出许多关于代码的某个特性对于客户端来说是否重要的决策,或者是否为实现细节。

然而,对于错误,有一个中间选择:您可以向阅读您代码的错误消息的人公开错误详细信息,而无需向客户端代码公开错误本身。一种方法是使用 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 方法。在这种情况下,我们建议

如果您无法控制所有可能具有属性 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 的一部分。