Error are values

2024-12-23 ⏳1.9分钟(0.8千字)

今天偶然在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 的例子

scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // 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
write := func(buf []byte) {
    if err != nil {
        return
    }
    _, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
    return err
}

这还不够好,因为函数头部暴漏了write的细节,代码看起来有点笨拙借助 Scan 的思路使其更加简洁 1. 定义了一个名为 errWriter 的对象

type errWriter struct {
    w   io.Writer
    err error
}

不需要考虑标准库的 Write 签名,小写是为了突出区别。 write 方法调用底层 Writer 的 Write 方法,并记录第一个发生的错误

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

一旦发生过错误,后续的操作就是无效的,错误原因会被保存

ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// and so on
if ew.err != nil {
    return ew.err
}

与闭包的写法相比,这种更加简洁,并且还可以提供更多功能,例如:他可以计算出累计写入了多少字节,合并写入等。事实上,这种模式在标准库中很常见,例如 archive/zip 和 net/http ,上述写法也是  bufio 包的 Writer 实现思路,只是在 Flush() 时才告知错误。这种方式很好,但是有一个很重大的缺点,就是你不知道是在哪一步发生的错误,这就需要更精细的做法。

结论

  1. 错误就是函数返回的一个值,每次使用这个值需要你编写重复的代码,试着简化它(给他包装一下,提供更易用的API)
  2. 无论如何,一定要检查返回的错误

参考

  1. https://go.dev/blog/errors-are-values
  2. https://jxck.hatenablog.com/entry/golang-error-handling-lesson-by-rob-pike