预定义的特定错误,我们叫为 sentinel error,这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法。所以对于 Go,我们使用特定的值来表示错误。
if err == ErrSomething { … }
类似的 io.EOF,更底层的 syscall.ENOENT。
使用 sentinel 值是最不灵活的错误处理策略,因为调用方必须使用 == 将结果与预先声明的值进行比较。当您想要提供更多的上下文时,这就出现了一个问题,因为返回一个不同的错误将破坏相等性检查。甚至是一些有意义的 fmt.Errorf 携带一些上下文,也会破坏调用者的 == ,调用者将被迫查看 error.Error() 方法的输出,以查看它是否与特定的字符串匹配
结论: 尽可能避免 sentinel errors。
我的建议是避免在编写的代码中使用 sentinel errors。在标准库中有一些使用它们的情况,但这不是一个您应该模仿的模式。
Error type 是实现了 error 接口的自定义类型。例如 MyError 类型记录了文件和行号以展示发生了什么。
因为 MyError 是一个 type,调用者可以使用断言转换成这个类型,来获取更多的上下文信息。
与错误值相比,错误类型的一大改进是它们能够包装底层错误以提供更多上下文。一个不错的例子就是 os.PathError 它提供了底层执行了什么操作、那个路径出了什么问题。
调用者要使用类型断言和类型 switch,就要让自定义的 error 变为 public。这种模型会导致和调用者产生强耦合,从而导致 API 变得脆弱。
结论是尽量避免使用 error types,虽然错误类型比 sentinel errors 更好,因为它们可以捕获关于出错的更多上下文,但是 error types 共享 error values 许多相同的问题。
因此,我的建议是避免错误类型,或者至少避免将它们作为公共 API 的一部分。
在我看来,这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。
我将这种风格称为不透明错误处理,因为虽然您知道发生了错误,但您没有能力看到错误的内部。作为调用者,关于操作的结果,您所知道的就是它起作用了,或者没有起作用(成功还是失败)。
这就是不透明错误处理的全部功能–只需返回错误而不假设其内容。
在少数情况下,这种二分错误处理方法是不够的。例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理。在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。考虑这个例子:
可以看到,外部需要依赖底层错误类型的Temporary()方法,可以在错误包中提供调用该方法判断的函数从而隐藏暴露的底层错误类型细节。
这里的关键是,这个逻辑可以在不导入定义错误的包或者实际上不了解 err 的底层类型的情况下实现——我们只对它的行为感兴趣
无错误的正常流程代码,将成为一条直线,而不是缩进的代码。
下面的代码有啥问题?
统计 io.Reader 读取内容的行数
上述代码中需要大量的判断err != nil
的场景,通过封装Writer
对象,可以讲对于err
的判断转移至实现的Write
函数中,改进版本如下:
还记得之前我们 auth 的代码吧,如果 authenticate 返回错误,则 AuthenticateRequest 会将错误返回给调用方,调用者可能也会这样做,依此类推。在程序的顶部,程序的主体将把错误打印到屏幕或日志文件中,打印出来的只是:没有这样的文件或目录。
没有生成错误的 file:line 信息。没有导致错误的调用堆栈的堆栈跟踪。这段代码的作者将被迫进行长时间的代码分割,以发现是哪个代码路径触发了文件未找到错误。
但是正如我们前面看到的,这种模式与 sentinel errors 或 type assertions 的使用不兼容,因为将错误值转换为字符串,将其与另一个字符串合并,然后将其转换回 fmt.Errorf 破坏了原始错误,导致等值判定失败。
you should only handle errors once. Handling an error means inspecting the error value, and making a single decision.
我们经常发现类似的代码,在错误处理中,带了两个任务: 记录日志并且再次返回错误。
在这个例子中,如果在 w.Write 过程中发生了一个错误,那么一行代码将被写入日志文件中,记录错误发生的文件和行,并且错误也会返回给调用者,调用者可能会记录并返回它,一直返回到程序的顶部。
上述代码输出结果为:
unable to write: io.EOF
could not write config: io.EOF
这种错误没有任何意义,无法帮助定位问题。同时,不应该在每一个内部调用返回 err 时就记录一次日志,而应该只在最外层的调用判断 err != nil
时记录日志,因为只有在这里记录的日志才是最完整的。
除了上述结果,程序员忘记将 err 进行 return 可能也会导致系统存在潜在的bug。
Go 中的错误处理契约规定,在出现错误的情况下,不能对其他返回值的内容做出任何假设。由于JSON 序列化失败,buf 的内容是未知的,可能它不包含任何内容,但更糟糕的是,它可能包含一个半写的 JSON 片段。
由于程序员在检查并记录错误后忘记 return,损坏的缓冲区将被传递给 WriteAll,这可能会成功,因此配置文件将被错误地写入。但是,该函数返回的结果是正确的。
日志记录与错误无关且对调试没有帮助的信息应被视为噪音,应予以质疑。记录的原因是因为某些东西失败了,而日志包含了答案。
使用 github.com/pkg/errors 库
fmt.Errorf 用了占位符 %w 之后创建的也是 wrapError 类型。上例中的Wrap方法调用处可改写为:fmt.Errorf(“%w\nopen failed”, err)
输出结果为:
通过使用 pkg/errors 包,您可以向错误值添加上下文,这种方式既可以由人也可以由机器检查。
只有最底层产生错误原因的位置才需要调用一次Wrap,上层链路调用该底层函数的位置只需要简单返回该err即可。
简单来说就是,错误发生的最底层Warp一下,中间层直接透传,最顶层打日志。
最简单的错误检查
有时我们需要对 sentinel error 进行检查
实现了 error interface 的自定义 error struct,进行断言使用获取更丰富的上下文
函数在调用栈中添加信息向上传递错误,例如对错误发生时发生的情况的简要描述。
使用创建新错误 fmt.Errorf 丢弃原始错误中除文本外的所有内容。正如我们在上面的 QueryError中看到的那样,我们有时可能需要定义一个包含底层错误的新错误类型,并将其保存以供代码检查。程序可以查看 QueryError 值以根据底层错误做出决策。这里是 QueryError:
go1.13为 errors 和 fmt 标准库包引入了新特性,以简化处理包含其他错误的错误。其中最重要的是: 包含另一个错误的 error 可以实现返回底层错误的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那么我们说 e1 包装 e2,您可以展开 e1 以获得 e2。
按照此约定,我们可以为上面的 QueryError 类型指定一个 Unwrap 方法,该方法返回其包含的错误:
go1.13 errors 包包含两个用于检查错误的新函数:Is 和 As。
如前所述,使用 fmt.Errorf 向错误添加附加信息。
在 Go 1.13 中 fmt.Errorf 支持新的 %w 谓词。
用 %w 包装错误可用于 errors.Is 以及 errors.As。
内部代码实现:
Is方法实现源码:
可以自定义一个Error类型,并实现它的Is方法:
注意:%w 不包含堆栈上下文,更推荐使用pkg\errors库的Wrap方法
可以使用pkg\errors的Wrap方法封装官方标准库的errors,从而实现打印堆栈信息。
https://www.infoq.cn/news/2012/11/go-error-handle/
https://golang.org/doc/faq#exceptions
https://www.ardanlabs.com/blog/2014/10/error-handling-in-go-part-i.html
https://www.ardanlabs.com/blog/2014/11/error-handling-in-go-part-ii.html
https://www.ardanlabs.com/blog/2017/05/design-philosophy-on-logging.html
https://medium.com/gett-engineering/error-handling-in-go-53b8a7112d04
https://medium.com/gett-engineering/error-handling-in-go-1-13-5ee6d1e0a55c
https://rauljordan.com/2020/07/06/why-go-error-handling-is-awesome.html
https://morsmachine.dk/error-handling
https://crawshaw.io/blog/xerrors
https://dave.cheney.net/2012/01/18/why-go-gets-exceptions-right
https://dave.cheney.net/2015/01/26/errors-and-exceptions-redux
https://dave.cheney.net/2014/11/04/error-handling-vs-exceptions-redux
https://dave.cheney.net/2014/12/24/inspecting-errors
https://dave.cheney.net/2016/04/07/constant-errors
https://dave.cheney.net/2019/01/27/eliminate-error-handling-by-eliminating-errors
https://dave.cheney.net/2016/06/12/stack-traces-and-the-errors-package
https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
https://blog.golang.org/errors-are-values
https://blog.golang.org/error-handling-and-go
https://blog.golang.org/go1.13-errors
https://commandcenter.blogspot.com/2017/12/error-handling-in-upspin.html