把 Go 错误当数值而不是异常

未匹配的标注

本文为官方 Go Blog 的中文翻译,详见 翻译说明

罗伯.派克
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 方法调用基础 WriterWrite 方法并记录第一个错误以供将来参考:

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.WriterWrite方法与上面的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()
}

至少对于某些应用程序,此方法存在一个明显的缺点:无法知道发生错误之前已完成多少处理。如果该信息很重要,则必须采用更细粒度的方法。但是,通常,最后进行全有或全无的检查就足够了。

我们只研究了一种避免重复错误处理代码的技术。请记住,使用errWriterbufio.Writer并不是简化错误处理的唯一方法,并且这种方法并不适合所有情况。但是,关键的教训是,错误是值,而Go编程语言的全部功能可用于处理它们。

使用该语言简化错误处理。

但是请记住:无论做什么,都要检查错误!

最后,有关我与@jxck_互动的完整故事,包括他录制的一些视频,请访问他的博客.

本文章首发在 LearnKu.com 网站上。

本译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://learnku.com/docs/go-blog/errors-...

译文地址:https://learnku.com/docs/go-blog/errors-...

上一篇 下一篇
Summer
贡献者:4
讨论数量: 0
发起讨论 只看当前版本


暂无话题~