本文摘自我最近在日本东京举行的GoCon春季会议上的演讲。
Errors are just values
我花了很多时间考虑Go程序中错误处理的最佳方法。我真希望存在单一的错误处理方式,可以通过死记硬背教给所有Go程序员,就像教数学或英文字母表一样。
但是,我得出结论,不存在单一的错误处理方式。 相反,我认为Go的错误处理可以分为三个核心策略。
Sentinel errors
第一类错误处理就是我所说的_sentinel errors_。
if err == ErrSomething { … }
该名称源于计算机编程中使用特定值的实践,表示不可能进一步处理。 因此,对于Go,我们使用特定值来表示错误。
例子包括 io.EOF
类的值,或低层级的错误,如 syscall
包中的常 syscall.ENOENT
。
甚至还有 sentinel errors
表示_没有_发生错误,比如 go/build.NoGoError
, 和 path/filepath.Walk
的 path/filepath.SkipDir
。
使用 sentinel
值是灵活性最小的错误处理策略,因为调用者必须使用等于运算符,将结果与预先声明的值进行比较。 当您想要提供更多上下文时就会出现问题,因为返回一个不同的错误会破坏相等检查。
即使是用心良苦的使用 fmt.Errorf
为错误添加一些上下文,将使调用者的相等测试失败。 调用者转而被迫查看 error
的 Error
方法的输出,以查看它是否与特定字符串匹配。
Never inspect the output of error.Error
另外,我认为永远不应该检查 error.Error
方法的输出。error
接口上的 Error
方法是为人类,而不是代码。
该字符串的内容属于日志文件,或显示在屏幕上。 您不应该尝试通过检查它以更改程序的行为。
我知道有时候这是不可能的,正如有人在推特上指出的那样,此建议并不适用于编写测试。 更重要的是,在我看来,比较错误的字符串形式是一种代码气味,你应该尽量避免它。
Sentinel errors become part of your public API
如果您的 public 函数或方法返回特定值的错误,那么该值必须是 public 的,当然还要有文档记录。 这会增加API的面积。
如果您的API定义了一个返回特定错误的接口,则该接口的所有实现都将被限制为仅返回该错误,即使它们可能提供更具描述性的错误。
通过 io.Reader
看到这一点 。 像 io.Copy
这样的函数,需要一个 reader 实现来_精确_地返回 io.EOF
,以便向调用者发出不再有数据的信号,但这不是错误 。
Sentinel errors create a dependency between two packages
到目前为止,sentinel error values
的最大问题是它们在两个包之间创建源代码依赖性。 例如,要检查错误是否等于 io.EOF
,您的代码必 import io
包。
这个具体示例听起来并不那么糟糕,因为它很常见,但想象一下,当项目中的许多包导出 error values
,项目中的其他包必须 import 以检查特定的错误条件时存在的耦合。
在一个玩弄这种模式的大型项目中工作过,我可以告诉你,以 import 循环的形式出现的糟糕设计的幽灵从未远离我们的脑海。
Conclusion: avoid sentinel errors
所以,我的建议是在你编写的代码中避免使用 sentinel error values
。 在某些情况下,它们会在标准库中使用,但你不应该模仿这种模式。
如果有人要求您从包中导出错误值,您应该礼貌地拒绝,而是建议一种替代方法,例如我将在下面讨论的方法。
Error types
Error types
是我想讨论的Go错误处理的第二种形式。
if err, ok := err.(SomeType); ok { … }
错误类型是您创建的实现错误接口的类型。 在此示例中,MyError
类型跟踪文件和行,以及解释所发生情况的消息。
type MyError struct { Msg string File string Line int } func (e *MyError) Error() string { return fmt.Sprintf("%s:%d: %s”, e.File, e.Line, e.Msg) } return &MyError{"Something happened", “server.go", 42}
由于 MyError error
是一种类型,因此调用者可以使用类型断言从错误中提取额外的上下文。
err := something() switch err := err.(type) { case nil: // call succeeded, nothing to do case *MyError: fmt.Println(“error occurred on line:”, err.Line) default: // unknown error }
error types
相对于 error values
的重大改进是,它们能够包装底层错误以提供更多上下文。
一个很好的例子是 os.PathError
类型,它通过它试图执行的操作和它试图使用的文件来注释底层错误。
// PathError records an error and the operation // and file path that caused it. type PathError struct { Op string Path string Err error // the cause } func (e *PathError) Error() string
Problems with error types
调用者可以使用类型断言或类型 switch,error types
必须是 public。
如果您的代码实现了一个接口,其契约需要特定的错误类型,则该接口的所有实现者都需要依赖于定义错误类型的包。
对包类型的深入了解,会建立与调用者很强耦合,从而形成一个脆弱的API。
Conclusion: avoid error types
虽然 error types
比 sentinel error values
更好,因为它们可以捕获更多关于错误的上下文,错误类型同样拥有许多 error values
的问题。
所以我的建议是避免 error types
,或者至少避免使它们成为公共API的一部分。