把 Go 错误当数值而不是异常
罗伯.派克
2015年1月25日
Go程序员(尤其是Go新手)之间的讨论共同点是如何处理错误。对话往往会因为下面序列的多次出现,而变成哀叹连连。
if err != nil {
return err
}
最近,我们扫描了所有可以找到的开放源代码项目,发现此代码段每页或每页仅出现一次,发生的频率比您认为的要少。但是, 如果仍然坚持认为必须一直输入
if err != nil
,一定有问题,而且显而易见的目标就是Go本身。
这是不幸的,令人误解的,并很容易可以纠正。也许正在发生的事情就是Go新手问 "如何处理错误?",学习这种模式,然后就此止步。在其他语言中,可能使用try-catch块或其他此类机制来处理错误。所以,程序员认为,当我用我的旧语言使用try-catch时,我只会在Go中键入 if
err
!=
nil
。随时时间流逝,Go代码会收集许多这样的代码片段,结果感觉很笨拙。
无论这种解释是否合适,这些Go程序员显然都错失了一个关于错误的基本要点: 错误就是值。
我们可以对值进行编程,并且由于错误是值,因此可以对错误进行编程。
当然,涉及到错误值的常见状态就是测试它是否为nil(空),但是使用错误值可以执行无数其他操作,并且应用其中的一些其他操作可以使你的程序更好,从而消除很多样板,如果每个错误都使用了一个死记硬背的if语句进行检查,则会出现这种情况。
这是来自bufio
包的(Scanner)扫描器
类型的简单示例。它的(Scan)扫描
方法执行基础I / O,这当然可能导致错误。但是Scan(扫描)
方法根本不会暴露任何错误。而是返回一个布尔值,并在扫描结束时运行一个单独的方法,报告是否发生错误。客户端代码如下所示:
scanner := bufio.NewScanner(input)
for scanner.Scan() {
token := scanner.Text()
// process token
}
if err := scanner.Err(); err != nil {
// process the error
}
当然,有一个nil检查是否有错误,但是它只出现一次并执行一次。可以将Scan
方法定义为:
func (s *Scanner) Scan() (token []byte, error)
然后示例用户代码可能是(取决于令牌的检索方式),
scanner := bufio.NewScanner(input)
for {
token, err := scanner.Scan()
if err != nil {
return err // or maybe break
}
// process token
}
这没有太大区别,但是有一个重要的区别。在此代码中,客户端必须在每次迭代中检查错误,但是在真实的Scanner
API中,错误处理是从关键API元素中抽象出来的,该关键API元素会遍历令牌。因此,使用真正的API,客户的代码会更加自然:循环直到完成,然后再担心错误。错误处理不会掩盖控制流程。
在幕后, 当然是 Scan
一旦遇到 I/O 错误, 它就会记录并返回 false
. 客户端询问时, 单独的方法 Err
报告错误值. 尽管这很琐碎, 但会有些不一样
if err != nil
到处或要求客户在每个令牌后检查错误. 它使用错误值进行编程. 是的, 很简明的编程, 但是仍然可以编程.
值得强调的是, 无论采用哪种设计, 程序都要检查错误, 无论它们是否暴露出来, 这一点至关重要. 这里的讨论不是关于如何避免检查错误, 而是关于使用语言以宽限期处理错误.
当我参加 2014 年秋季在东京举行的 GoCon 时, 出现了重复性错误检查代码的话题. 一位在 Twitter 上@jxck
热情的 gopher 表达了对错误检查的熟悉的哀叹. 他有一些看起来像这样的代码:
_, 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])
// 稍等
if err != nil {
return err
}
这种模式很好用, 但是在执行写操作的每个函数中都需要一个闭包. 使用单独的辅助函数比较麻烦, 因为 err
变量需要在调用之间进行维护 (值得尝试).
通过借鉴上述 Scan
函数的思路, 我们可以使这种方法更简洁, 更通用和可重用. 我在讨论中提到了这种技术, 但是 @jxck_
看不到如何应用它. 经过长时间的交流, 由于语言障碍, 我问我是否可以借用他的笔记本电脑并通过输入一些代码向他展示.
我定义了一个名为 errWriter
的对象, 如下所示:
type errWriter struct {
w io.Writer
err error
}
并给了它一种方法 write
. 它不需要具有标准的 Write
签名, 并且在某种程度上小写以突出区别. write
方法调用基础 Writer
的 Write
方法并记录第一个错误以供将来参考:
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
一旦发生错误,write
方法将变为无操作, 但将保存错误值.
给定 errWriter
类型及其 write
方法, 可以将以上代码重构:
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// 稍等
if ew.err != nil {
return ew.err
}
与使用闭包相比, 这也更简洁, 并且还使得实际的写入顺序在页面上更容易看到. 不再混乱. 使用错误值 (和接口) 进行编程使代码更美观.
同一软件包中的其他一些代码很可能可以基于此想法, 甚至可以直接使用 errWriter
.
而且, 一旦 errWriter
存在, 它可以做更多的事情来提供帮助, 尤其是在较少人工的示例中. 它可能会累积字节数. 它可以将写入合并到单个缓冲区中, 然后可以原子传输该缓冲区. 还有更多.
实际上,这种模式经常出现在标准库中。 archive/zip
和 net/http
包都使用它。对此讨论更重要的是, bufio
包的 Writer
实际上是 errWriter
想法的实现。 尽管 bufio.Writer.Write
返回一个错误,但这主要是关于尊重io.Writer
接口。 bufio.Writer
的Write
方法与上面的errWriter.write
方法类似,其中Flush
报告错误,因此我们的示例可以这样写:
b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// 等等
if b.Flush() != nil {
return b.Flush()
}
至少对于某些应用程序,此方法存在一个明显的缺点:无法知道发生错误之前已完成多少处理。如果该信息很重要,则必须采用更细粒度的方法。但是,通常,最后进行全有或全无的检查就足够了。
我们只研究了一种避免重复错误处理代码的技术。请记住,使用errWriter
或bufio.Writer
并不是简化错误处理的唯一方法,并且这种方法并不适合所有情况。但是,关键的教训是,错误是值,而Go编程语言的全部功能可用于处理它们。
使用该语言简化错误处理。
但是请记住:无论做什么,都要检查错误!
最后,有关我与@jxck_互动的完整故事,包括他录制的一些视频,请访问他的博客.
本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。