从Go 2 Error Proposal谈起
Go
对error
的处理一直都是很大的争议点,这点官方也已多次发文,并在2019年1月推出了一篇Proposal,有兴趣的可以点击链接细细品读。
下面,我会结合Proposal原文,发表一些自己的看法(会带上主观意见),欢迎讨论。
目标
这篇Proposal有一句话很好地解释了对error
的期许:
making errors more informative for both programs and people
错误不仅是告诉机器怎么做的,也是告诉人发生了什么问题。
回顾
先让我们一起简单地回顾一下error
的现状,来更好地理解这个 more informative 指的是什么。
原始的error定义为:
1 | type error interface { |
这里面的包含信息很少:一个Error() 的方法,即用字符串返回对应的错误信息。
最常用的error
相关方法是2种:
- 创建
error
-fmt.Errorf
,它是针对Error()
方法返回的字符串进行加工,如附带一些参数信息(暂不讨论%w这个wrap错误的实现) - 使用
error
- 由于我们将error
的输出结果定义为字符串,所以使用error
时,一旦涉及到细节,就只能使用一些string
的方法了
举个具体的例子:
1 | func main() { |
这里存在3个明显的问题:
- 破坏性 -
fmt.Errorf
破坏了原有的error,将它从一个 具体对象 转化为 扁平的string
,再填充到了新的error
中。所以,通过fmt.Errorf
处理后的error,都只传递了一个string
的信息 - 实现僵化 - “no such file or directory” 这个错误信息用的是硬编码,对第三方
readFile
的内容有强依赖,不灵活 - 排查问题效率低 - 可以通过日志组件了解到error在
main
函数哪行发生,但无法知道错误从readFile
中的哪行返回过来的
其中第一个破坏性的问题,其实就是破坏了error这个interface背后的具体实现,违背了面向对象的继承原则。
Handle Errors Only Once
在工程中,为了解决 排查问题效率低 这个问题,有一个很常见的做法(以上面的readFile为例):
1 | func readFile(fileName string) ([]byte, error) { |
没错,就是 打印错误并返回。有大量排查问题经验的同学,对此肯定是深恶痛绝: 一个错误能找到N处打印,看得人眼花缭乱。
这里违背了一个关键性的原则:对错误只进行一次处理,处理完之后就不要再往上抛了,而打印错误也是一种处理。
结合三种具体的场景,我们分析一下:
- 一个程序模块内,
error
不断往上抛,最上层处理; - 一个公共的工具包中,
error
不记录,传给调用方处理; - 一个RPC模块的调用中,
error
可以记录,作为debug
信息,而具体的处理仍应交给调用方。
示例参考文章
- https://dave.cheney.net/2016/04/27/dont-just-check-errors-handle-them-gracefully
- https://www.orsolabs.com/post/go-errors-and-logs/
理论实现
那么,怎么样的error
才是合适的呢?我们分两个角度来看这个error
:
- 对程序来说,
error
要包含错误细节:如错误类型、错误码等,方便在模块间传递; - 对人来说,
error
要包含代码信息:如相关的调用参数、运行信息,方便查问题;
用原文一句话来归纳:hide implementation details from programs while displaying them for diagnosis
- Wrap - 隐藏实现,针对代码调用时的堆栈信息
- Is/As - 展示细节,针对底层真正实现的数据结构
当前实现
Go
语言发展多年,已经有了很多关于error
的处理方法,但大多为过渡方案,我就不一一分析了。
这里我以 github.com/pkg/errors 为例,也是这个官方Proposal的重点参考对象,简单地分享一下大致实现思路。
代码量并不多,大家可以自行阅读源码:
New 产生错误的堆栈信息
1 | func New(message string) error { |
关键点 stack保存了错误产生的堆栈信息,如函数名、代码行
Wrap 包装错误
1 | func Wrap(err error, message string) error { |
关键点 将错误包装出一个全新的堆栈。一般只用于对外接口产生错误时,包括标准库、RPC。
WithMessage 添加普通信息
1 | func WithMessage(err error, message string) error { |
关键点 添加错误信息,增加一个普通的堆栈打印
Is 解析Sentinel错误、即全局错误变量
1 | func Is(err, target error) bool { return stderrors.Is(err, target) } |
关键点 反复Unwrap、提取错误,解析并对比错误类型
As - 提取出具体的错误数据结构
1 | func As(err error, target interface{}) bool { return stderrors.As(err, target) } |
关键点 反复Unwrap、提取错误,提取底层的实现类型
小结
Go
语言对error
的定义很简单,虽然带来了灵活性,但也导致处理方式泛滥,一如当年的Go语言的版本管理。如今的go mod版本管理机制已经”一统江湖“,随着大家对error
这块的不断深入,Error Handling
也总会达成共识。
接下来,我会结合实际代码样例,写一个具体工程中 Error Handling 的操作方法,提供一定的参考。
Github: https://github.com/Junedayday/code_reading
Blog: http://junes.tech/
Bilibili: https://space.bilibili.com/293775192
公众号: golangcoding