Go语言错误处理的姿势
Go语言的错误处理常常给新手和有其他语言背景的使用者带来疑惑,在这篇文章中,我们将区分错误(error
)和异常(panic
),讨论什么样的错误是“好”的(容易检查和排错),介绍一种让错误变“好”的常用方式(fmt.Errorf()
)。
对了,如果你有其他编程语言背景,并且才接触Go不久,推荐阅读我先前的文章《Golang第二语言指南》。
本文中的“用户”意为编写代码的人(也就是你啦)。
区分异常(panic)和错误(error)
Go把表示程序遇到意外情况的方式分成了两种:panic
(异常)和error
(错误)。
就像Java/JS里的throw
,Python里的raise
,panic
会中止程序执行进入异常处理逻辑,异常可以在当前函数或者调用链向上的任何一层被defer recover
捕获处理,没被捕获的panic
会造成程序打印堆栈后异常退出。panic
很少会在用户代码中出现,一般用来表示除数为0、内存不足、强制类型转换失败等语言层级的异常,它们一般少见,如果出现往往意味着用户本身的实现问题,用户一般不会着重考虑和防御它们。
error
就显得平凡地多了,所有满足error
这个interface的值都可以当做一个合法的错误:
|
|
error
是一个平凡的值,运行时不会用特殊的逻辑对待它,不处理它不会让程序打印堆栈异常退出。
error
会在用户代码中大量出现,用来表示比如连接超时、JSON解析失败、文件不存在这些用户层级的错误,它们常见,往往和业务直接相关,用户一般着重考虑防御它们。
总结下:
panic
:不着重考虑防御的异常,没有明确的抛出位置;error
:用户着重考虑防御的错误,抛出位置固定而明显,只要函数返回值元组中最后一个是error
类型,用户就需要考虑防御和处理。
换句话说,Go语言中error
是值这个特征在鼓励用户考虑和处理每个错误,写出鲁棒健壮的程序。
我们喜欢“好”错误
错误也分好坏?当然。
举个例子,你名下有个天气API服务,它在收到请求时会查询Redis中有没有对应缓存,在没有缓存时再请求外部服务,最后返回结果给调用方。有一天,调用方找到你,说接口不工作了,你查了日志,错误信息是context deadline exceeded
,这个错误意味着代码里有地方发生了超时,可是在哪里呢?是缓存还是外部API?
好的错误是有根本原因和调用链上下文的错误,这样的错误容易排查,不需要去猜,试想如果你看到的错误信息是这样的:
reading cache: redis GET: context deadline exceeded
或者带有调用堆栈的:
context deadline exceeded
goroutine 1 [running]:
main.Example(0x19010001)
/Users/hello/main.go
temp/main.go:8 +0x64
main.main()
/Users/bill/main.go
temp/main.go:4 +0x32
那么你的排错工作都会容易得多。
实际应用中,我更喜欢第一种方式,比起堆栈,人工添加的文本错误上下文更易于阅读,有着更高的信息密度,而且看到的人就算没有接触相关代码也有可能理解错误原因。
为错误提供文本上下文
Go 1.13(2019年9月)中新增了fmt.Errorf()
用于为错误提供上下文,errors标准库新增Is()
, As()
, Unwrap()
用于便利化错误的鉴别和比较(这是另外一个错误处理的范畴了,本文按下不表)。
这个方案是讨论了很久才决定下来的,Go维护者们对于新特性的引入一直以来都比较谨慎。
在Go 1.13之前,社区内有各式各样自己的尝试,比较有名的是第三方库pkg/errors,它同时探索了添加上下文和添加堆栈两个方向。
fmt.Errorf()
的用法大概是这样的,回忆刚刚我们想要的那种“好错误”:
reading cache: redis GET: context deadline exceeded
这个错误就像在说故事一样,从左到右层层递进,每层fmt.Errorf()
叙述自己想要做的事情,然后用:
分隔下一层,下一层用%w
指代。下面这段伪代码中,FindWeather()
调用ReadCache()
,请试想ReadCache()
报错时,FindWeather()
调用者收到的错误:
|
|
诚然if err != nil
和fmt.Errorf()
这样的重复要打不少字(有得有失嘛),你可以看看如何配置自己喜欢的IDE的自动完成功能为你省下一些键盘敲击。
Go语言的静态分析工具可以为你检查出代码中忘记处理的错误(有时还挺关键的),不规范的错误上下文格式等(%d
,%f
, %v
, %w
, %x
傻傻分不清),我一般使用它们的合集版本golangci-lint.
其他为错误添加上下文的方式
虽说官方把错误文本上下文加入标准库算是投了这个方案一票,但是如果有必要,你也可以使用其他为错误添加上下文的方案,这里列了一些例子:
- 使用pkg/errors为错误添加堆栈;
- 如果是HTTP服务,使用panic和recover中间件,知乎的实现就是个例子,我猜想上下文也是以堆栈的方式呈现。
参考资料
更新日志
- (2021/10)感谢V2EX网友Mitt对文中示例伪代码的勘误;
- (2021/10)感谢V2EX网友ZSeptember对文章标题和主旨的建议。