Error are values
jeffrey今天偶然在go blog逛到一篇之前的技术文章,很惭愧居然没有拜读过原文(看过别人搬运的,但是居然不知道出自Rob Pike)讨论的内容就是很多人诟病go语言的 if err != nil
模版代码。
最开始写Go语言的时候,几乎会满屏的 if err != nil
, 所以我当时也有疑惑,这块该怎么处理,难道只能借助IDE的代码片段快速输入仅此而已吗?
万幸的是Rob Pike直接给出了答案和例子:Error are values
是的,错误就是一个值而已,你依然可以对他编程(指的是,可以对它添加方法、添加字段等),而不仅仅是判断他是否为 nil,不要把他当做exception,从而 catch 住然后记录一些东西然后继续让函数运行或中断逻辑。
原文摘要
Rob Pike : 虽然扫描了能找得到的开源项目代码,发现这种代码片段每1~2页才出现一次,必某些人认为的要少,但是尽管如此,如果人们仍然认为必须每次都要判断
if err != nil
, 那么肯定是哪里出了问题例子:原文中给出了标准库bufio Scan
的例子
:= bufio.NewScanner(input)
scanner for scanner.Scan() {
:= scanner.Text()
token // process token
}
if err := scanner.Err(); err != nil {
// process the error
}
这段例子中 if err
只出现了一次,按照我以前的写法,估计会在循环中判断读取有没有出错,有则 break 走处理异常逻辑分支
第二个例子则更加典型(这是 2014 年秋季 GoCon 时@jxck_和Rob Pike讨论错误处理的代码)
, err = fd.Write(p0[a:b])
_if err != nil {
return err
}
, err = fd.Write(p1[c:d])
_if err != nil {
return err
}
, err = fd.Write(p2[e:f])
_if err != nil {
return err
}
// and so on
首先这可以定义一个匿名的辅助函数进行重构
var err error
:= func(buf []byte) {
write if err != nil {
return
}
, err = w.Write(buf)
_}
(p0[a:b])
write(p1[c:d])
write(p2[e:f])
write// and so on
if err != nil {
return err
}
这还不够好,因为函数头部暴漏了write的细节,代码看起来有点笨拙借助 Scan 的思路使其更加简洁 1. 定义了一个名为 errWriter 的对象
type errWriter struct {
.Writer
w ioerror
err }
不需要考虑标准库的 Write
签名,小写是为了突出区别。 write
方法调用底层 Writer
的 Write
方法,并记录第一个发生的错误
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
, ew.err = ew.w.Write(buf)
_}
一旦发生过错误,后续的操作就是无效的,错误原因会被保存
:= &errWriter{w: fd}
ew .write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
ew// and so on
if ew.err != nil {
return ew.err
}
与闭包的写法相比,这种更加简洁,并且还可以提供更多功能,例如:他可以计算出累计写入了多少字节,合并写入等。事实上,这种模式在标准库中很常见,例如 archive/zip
和 net/http
,上述写法也是 bufio
包的 Writer
实现思路,只是在 Flush()
时才告知错误。这种方式很好,但是有一个很重大的缺点,就是你不知道是在哪一步发生的错误,这就需要更精细的做法。
结论
- 错误就是函数返回的一个值,每次使用这个值需要你编写重复的代码,试着简化它(给他包装一下,提供更易用的API)
- 无论如何,一定要检查返回的错误
参考
- https://go.dev/blog/errors-are-values
- https://jxck.hatenablog.com/entry/golang-error-handling-lesson-by-rob-pike