【go学习笔记】Go errors 最佳实践

文章目录

    • 一、Error Type
      • 1. Sentinel Error(预定义Error字符串错误值)
        • 1.1 缺点
      • 2. Error types(错误类型)
        • 2.1 缺点
      • 3. Opaque errors(不透明错误)
        • 3.1 Assert errors for behaviour, not type
    • 二、Handling Error
      • 1. Indented flow is for errors
      • 2. Eliminate error handling by eliminating errors
        • 2.1 示例1
        • 2.2 示例2
        • 2.3 示例3
      • 3. Wrap erros
        • 3.1 存在的问题1
        • 3.2 存在的问题2
        • 3.3 pkg/errors 库
        • 3.4 pkg/errors 库最佳实践(最重要)
        • 3.5 总结
    • 三、Go 1.13 errors
      • 1. Errors before Go 1.13
      • 2. Go1.13 errors标准库
        • 2.1 Unwrap
        • 2.2 Wrapping errors with %w
        • 2.3 Customizing error tests with Is and As methods
        • 2.4 Errors and package APIs
        • 2.5 errors & github.com/pkg/errors
    • 四、References

一、Error Type

1. Sentinel Error(预定义Error字符串错误值)

预定义的特定错误,我们叫为 sentinel error,这个名字来源于计算机编程中使用一个特定值来表示不可能进行进一步处理的做法。所以对于 Go,我们使用特定的值来表示错误。

if err == ErrSomething {}

类似的 io.EOF,更底层的 syscall.ENOENT。

【go学习笔记】Go errors 最佳实践_第1张图片

使用 sentinel 值是最不灵活的错误处理策略,因为调用方必须使用 == 将结果与预先声明的值进行比较。当您想要提供更多的上下文时,这就出现了一个问题,因为返回一个不同的错误将破坏相等性检查。甚至是一些有意义的 fmt.Errorf 携带一些上下文,也会破坏调用者的 == ,调用者将被迫查看 error.Error() 方法的输出,以查看它是否与特定的字符串匹配

1.1 缺点
  • 依赖检查 error.Error 的输出。
    不应该依赖检测 error.Error 的输出,Error 方法存在于 error 接口主要用于方便程序员使用,但不是程序(编写测试可能会依赖这个返回)。这个输出的字符串用于记录日志、输出到 stdout 等
  • Sentinel errors 成为你 API 公共部分。
    如果您的公共函数或方法返回一个特定值的错误,那么该值必须是公共的,当然要有文档记录,这会增加API 的表面积。
    如果 API 定义了一个返回特定错误的 interface,则该接口的所有实现都将被限制为仅返回该错误,即使它们可以提供更具描述性的错误。比如 io.Reader。像 io.Copy 这类函数需要 reader 的实现者比如返回 io.EOF 来告诉调用者没有更多数据了,但这又不是错误。
  • Sentinel errors 在两个包之间创建了依赖。
    sentinel errors 最糟糕的问题是它们在两个包之间创建了源代码依赖关系。例如,检查错误是否等于io.EOF,您的代码必须导入 io 包。这个特定的例子听起来并不那么糟糕,因为它非常常见,但是想象一下,当项目中的许多包导出错误值时,存在耦合,项目中的其他包必须导入这些错误值才能检查特定的错误条件(in the form of an import loop)。

结论: 尽可能避免 sentinel errors。
我的建议是避免在编写的代码中使用 sentinel errors。在标准库中有一些使用它们的情况,但这不是一个您应该模仿的模式。

2. Error types(错误类型)

Error type 是实现了 error 接口的自定义类型。例如 MyError 类型记录了文件和行号以展示发生了什么。
【go学习笔记】Go errors 最佳实践_第2张图片
因为 MyError 是一个 type,调用者可以使用断言转换成这个类型,来获取更多的上下文信息。

【go学习笔记】Go errors 最佳实践_第3张图片
与错误值相比,错误类型的一大改进是它们能够包装底层错误以提供更多上下文。一个不错的例子就是 os.PathError 它提供了底层执行了什么操作、那个路径出了什么问题。
【go学习笔记】Go errors 最佳实践_第4张图片

2.1 缺点

调用者要使用类型断言和类型 switch,就要让自定义的 error 变为 public。这种模型会导致和调用者产生强耦合,从而导致 API 变得脆弱。
结论是尽量避免使用 error types,虽然错误类型比 sentinel errors 更好,因为它们可以捕获关于出错的更多上下文,但是 error types 共享 error values 许多相同的问题。
因此,我的建议是避免错误类型,或者至少避免将它们作为公共 API 的一部分。

3. Opaque errors(不透明错误)

在我看来,这是最灵活的错误处理策略,因为它要求代码和调用者之间的耦合最少。
我将这种风格称为不透明错误处理,因为虽然您知道发生了错误,但您没有能力看到错误的内部。作为调用者,关于操作的结果,您所知道的就是它起作用了,或者没有起作用(成功还是失败)。
这就是不透明错误处理的全部功能–只需返回错误而不假设其内容。

【go学习笔记】Go errors 最佳实践_第5张图片

3.1 Assert errors for behaviour, not type

在少数情况下,这种二分错误处理方法是不够的。例如,与进程外的世界进行交互(如网络活动),需要调用方调查错误的性质,以确定重试该操作是否合理。在这种情况下,我们可以断言错误实现了特定的行为,而不是断言错误是特定的类型或值。考虑这个例子:
【go学习笔记】Go errors 最佳实践_第6张图片
【go学习笔记】Go errors 最佳实践_第7张图片
可以看到,外部需要依赖底层错误类型的Temporary()方法,可以在错误包中提供调用该方法判断的函数从而隐藏暴露的底层错误类型细节。
【go学习笔记】Go errors 最佳实践_第8张图片
这里的关键是,这个逻辑可以在不导入定义错误的包或者实际上不了解 err 的底层类型的情况下实现——我们只对它的行为感兴趣

二、Handling Error

1. Indented flow is for errors

无错误的正常流程代码,将成为一条直线,而不是缩进的代码。

【go学习笔记】Go errors 最佳实践_第9张图片
这里更推荐第一种判断错误的方式。

2. Eliminate error handling by eliminating errors

2.1 示例1

下面的代码有啥问题?

【go学习笔记】Go errors 最佳实践_第10张图片
上述代码可以简化为以下写法:
【go学习笔记】Go errors 最佳实践_第11张图片

2.2 示例2

统计 io.Reader 读取内容的行数

【go学习笔记】Go errors 最佳实践_第12张图片
改进版本:

【go学习笔记】Go errors 最佳实践_第13张图片

2.3 示例3

【go学习笔记】Go errors 最佳实践_第14张图片
上述代码中需要大量的判断err != nil的场景,通过封装Writer对象,可以讲对于err的判断转移至实现的Write函数中,改进版本如下:

【go学习笔记】Go errors 最佳实践_第15张图片
【go学习笔记】Go errors 最佳实践_第16张图片

3. Wrap erros

3.1 存在的问题1

还记得之前我们 auth 的代码吧,如果 authenticate 返回错误,则 AuthenticateRequest 会将错误返回给调用方,调用者可能也会这样做,依此类推。在程序的顶部,程序的主体将把错误打印到屏幕或日志文件中,打印出来的只是:没有这样的文件或目录。
【go学习笔记】Go errors 最佳实践_第17张图片
没有生成错误的 file:line 信息。没有导致错误的调用堆栈的堆栈跟踪。这段代码的作者将被迫进行长时间的代码分割,以发现是哪个代码路径触发了文件未找到错误。
【go学习笔记】Go errors 最佳实践_第18张图片
但是正如我们前面看到的,这种模式与 sentinel errors 或 type assertions 的使用不兼容,因为将错误值转换为字符串,将其与另一个字符串合并,然后将其转换回 fmt.Errorf 破坏了原始错误,导致等值判定失败。

3.2 存在的问题2

you should only handle errors once. Handling an error means inspecting the error value, and making a single decision.
【go学习笔记】Go errors 最佳实践_第19张图片
我们经常发现类似的代码,在错误处理中,带了两个任务: 记录日志并且再次返回错误。
【go学习笔记】Go errors 最佳实践_第20张图片
在这个例子中,如果在 w.Write 过程中发生了一个错误,那么一行代码将被写入日志文件中,记录错误发生的文件和行,并且错误也会返回给调用者,调用者可能会记录并返回它,一直返回到程序的顶部。
【go学习笔记】Go errors 最佳实践_第21张图片
【go学习笔记】Go errors 最佳实践_第22张图片
上述代码输出结果为:

unable to write: io.EOF
could not write config: io.EOF

这种错误没有任何意义,无法帮助定位问题。同时,不应该在每一个内部调用返回 err 时就记录一次日志,而应该只在最外层的调用判断 err != nil 时记录日志,因为只有在这里记录的日志才是最完整的。

除了上述结果,程序员忘记将 err 进行 return 可能也会导致系统存在潜在的bug。

Go 中的错误处理契约规定,在出现错误的情况下,不能对其他返回值的内容做出任何假设。由于JSON 序列化失败,buf 的内容是未知的,可能它不包含任何内容,但更糟糕的是,它可能包含一个半写的 JSON 片段。
由于程序员在检查并记录错误后忘记 return,损坏的缓冲区将被传递给 WriteAll,这可能会成功,因此配置文件将被错误地写入。但是,该函数返回的结果是正确的。

【go学习笔记】Go errors 最佳实践_第23张图片

3.3 pkg/errors 库

日志记录与错误无关且对调试没有帮助的信息应被视为噪音,应予以质疑。记录的原因是因为某些东西失败了,而日志包含了答案。

  • The error has been logged.(错误要被日志记录)
  • The application is back to 100% integrity. (应用程序处理错误,保证100%完整性)
  • The current error is not reported any longer. (之后不再报告当前错误)

使用 github.com/pkg/errors 库

【go学习笔记】Go errors 最佳实践_第24张图片

fmt.Errorf 用了占位符 %w 之后创建的也是 wrapError 类型。上例中的Wrap方法调用处可改写为:fmt.Errorf(“%w\nopen failed”, err)

输出结果为:
【go学习笔记】Go errors 最佳实践_第25张图片
通过使用 pkg/errors 包,您可以向错误值添加上下文,这种方式既可以由人也可以由机器检查。
【go学习笔记】Go errors 最佳实践_第26张图片
【go学习笔记】Go errors 最佳实践_第27张图片

3.4 pkg/errors 库最佳实践(最重要)

只有最底层产生错误原因的位置才需要调用一次Wrap,上层链路调用该底层函数的位置只需要简单返回该err即可。

  • 在你的应用代码中,使用 errors.New 或者 errors.Errorf 返回错误。
    【go学习笔记】Go errors 最佳实践_第28张图片
  • 如果调用其他包内的函数,通常简单的直接返回。
    【go学习笔记】Go errors 最佳实践_第29张图片
  • 如果和其他第三方库进行协作,考虑使用 errors.Wrap 或者 errors.Wrapf 保存堆栈信息。同样适用于和标准库协作的时候。
    【go学习笔记】Go errors 最佳实践_第30张图片
  • 非底层真正产生错误的位置直接返回错误,而不是每个错误产生的地方到处打日志。
  • 在程序的顶部或者是工作的 goroutine 顶部(请求入口),使用 %+v 把堆栈详情记录。
    【go学习笔记】Go errors 最佳实践_第31张图片
  • 使用 errors.Cause 获取 root error,再进行和 sentinel error 判定。
3.5 总结
  • Packages that are reusable across many projects only return root error values.
    选择 wrap error 是只有 applications 可以选择应用的策略。具有最高可重用性的包只能返回根错误值。此机制与 Go 标准库中使用的相同(kit 库的 sql.ErrNoRows)。Warpped error应该只有业务代码才应该使用
  • If the error is not going to be handled, wrap and return up the call stack.
    这是关于函数/方法调用返回的每个错误的基本问题。如果函数/方法不打算处理错误,那么用足够的上下文 wrap errors 并将其返回到调用堆栈中。例如,额外的上下文可以是使用的输入参数或失败的查询语句。确定您记录的上下文是足够多还是太多的一个好方法是检查日志并验证它们在开发期间是否为您工作。
  • Once an error is handled, it is not allowed to be passed up the call stack any longer.
    一旦确定函数/方法将处理错误,错误就不再是错误。如果函数/方法仍然需要发出返回,则它不能返回错误值。它应该只返回零(比如降级处理中,你返回了降级数据,然后需要 return nil)。

简单来说就是,错误发生的最底层Warp一下,中间层直接透传,最顶层打日志。

三、Go 1.13 errors

1. Errors before Go 1.13

最简单的错误检查
【go学习笔记】Go errors 最佳实践_第32张图片
有时我们需要对 sentinel error 进行检查

【go学习笔记】Go errors 最佳实践_第33张图片
实现了 error interface 的自定义 error struct,进行断言使用获取更丰富的上下文
【go学习笔记】Go errors 最佳实践_第34张图片
函数在调用栈中添加信息向上传递错误,例如对错误发生时发生的情况的简要描述。
在这里插入图片描述
使用创建新错误 fmt.Errorf 丢弃原始错误中除文本外的所有内容。正如我们在上面的 QueryError中看到的那样,我们有时可能需要定义一个包含底层错误的新错误类型,并将其保存以供代码检查。程序可以查看 QueryError 值以根据底层错误做出决策。这里是 QueryError:
【go学习笔记】Go errors 最佳实践_第35张图片
在这里插入图片描述

2. Go1.13 errors标准库

2.1 Unwrap

go1.13为 errors 和 fmt 标准库包引入了新特性,以简化处理包含其他错误的错误。其中最重要的是: 包含另一个错误的 error 可以实现返回底层错误的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那么我们说 e1 包装 e2,您可以展开 e1 以获得 e2。
按照此约定,我们可以为上面的 QueryError 类型指定一个 Unwrap 方法,该方法返回其包含的错误:
在这里插入图片描述
go1.13 errors 包包含两个用于检查错误的新函数:Is 和 As。
【go学习笔记】Go errors 最佳实践_第36张图片
在这里插入图片描述
【go学习笔记】Go errors 最佳实践_第37张图片

2.2 Wrapping errors with %w

如前所述,使用 fmt.Errorf 向错误添加附加信息。
【go学习笔记】Go errors 最佳实践_第38张图片
在 Go 1.13 中 fmt.Errorf 支持新的 %w 谓词。
【go学习笔记】Go errors 最佳实践_第39张图片
用 %w 包装错误可用于 errors.Is 以及 errors.As。
在这里插入图片描述
内部代码实现:
【go学习笔记】Go errors 最佳实践_第40张图片

2.3 Customizing error tests with Is and As methods

Is方法实现源码:
【go学习笔记】Go errors 最佳实践_第41张图片
可以自定义一个Error类型,并实现它的Is方法:
【go学习笔记】Go errors 最佳实践_第42张图片

2.4 Errors and package APIs

之前写法与现在写法对比:
【go学习笔记】Go errors 最佳实践_第43张图片

注意:%w 不包含堆栈上下文,更推荐使用pkg\errors库的Wrap方法

2.5 errors & github.com/pkg/errors

可以使用pkg\errors的Wrap方法封装官方标准库的errors,从而实现打印堆栈信息。
【go学习笔记】Go errors 最佳实践_第44张图片

四、References

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

你可能感兴趣的:(Golang,golang,学习,笔记,后端)