Go 错误是值
本文是对 Rob Pike 博客 Errors are values 的翻译。
在 Go 中,经常可以看到关于错误处理的讨论。在讨论的过程中,对话通常会演变成对语句
1 |
|
出现次数的哀叹。我们最近扫描了我们能找到的所有开源项目,发现这个代码段在每一页只出现过一两次,比一些人让你相信的要少。不过,如果人们仍然认为你必须一直输入
1 |
|
这种事情发生一定是有原因的,一个显而易见的原因就是 Go 语言本身。
这是一个不幸的,具有误导性的结论,并且它很容易被纠正。一个可能发生的情况是,刚接触 Go 的程序员会问,“如何处理错误?“,在学会了上面的这种处理模式后,就止步不前了。在其他语言中,例如 java,人们可能会使用 try-catch 块或其他此类机制来处理错误。因此,程序员会认为,就像我以前用过的语言中使用 try-catch 一样,我只需在 Go 中输入 if
err
!=
nil
。随着时间的推移,代码库中累积了许多这样的代码段,最终让人觉得笨拙。
无论这种解释是否合适,显然这些 Go 程序员都忽略了关于错误的一个基本要点:错误是值。
我们可以对值进行编程,并且由于错误是值,因此可以对错误进行编程。
当然,涉及错误值的一个常见语句是判断它是否为 nil,但还可以对错误值执行无数其他操作,并且应用其中一些操作可以使您的程序更好,从而消除如果使用死板的 if 语句检查每个错误时出现的许多样板代码。
下面是 bufio
包的 Scanner
类型的简单示例。它的 Scan
方法执行基础 I/O,这当然可能导致错误。然而, Scan
方法根本不公开错误。相反,它返回一个布尔值,并在扫描结束时运行一个单独的方法来报告是否发生错误。客户端代码如下所示:
1 |
|
当然,在代码中有一个错误的 nil 检查,但它只出现并执行一次。 Scan
方法本可以定义为
1 |
|
然后示例用户代码可能是(取决于如何检索 Token ),
1 |
|
这并没有什么不同,但有一个重要的区别。在此代码中,客户端必须在每次迭代时都检查一次错误,但在真正的 Scanner
API 中,错误处理从关键的 API 元素(即遍历标记)中抽象出来。因此,使用真正的 API 时,客户端的代码感觉更自然:循环直到完成,然后处理错误。错误处理不会掩盖控制流。
当然,在底层发生的事情是,一旦 Scan
遇到 I/O 错误,它就会记录该错误并返回 false
。当客户端询问时,另一个方法 Err
会报告错误值。虽然这很微不足道,但它与在每个地方,都放置
1 |
|
或要求客户端在每个 Token 后检查错误不同。这是使用错误值进行编程。简单的编程,是的,但仍然是编程。
值得强调的是,无论设计如何,程序都必须检查错误,无论它们如何暴露。这里的讨论不是关于如何避免检查错误,而是关于使用该语言优雅地处理错误。
我在 2014 年秋季参加东京举行的 GoCon 时,重复错误检查代码的话题出现了。一位热情的 gopher,他在 Twitter 上的昵称是 @jxck_
,对错误检查发出了熟悉的抱怨。他有一些代码,从形式上看类似这样:
1 |
|
它非常重复。在更长的实际代码中,还有更多内容,因此不能简单地使用辅助函数重构它,但这种理想化的形式,一个封闭在错误变量上的函数字面量会有所帮助:
1 |
|
这种模式运行良好,但需要在执行写入的每个函数中使用闭包;单独的辅助函数使用起来比较笨拙,因为 err
变量需要在调用之间保持(试试看)。
我们可以借用上面 Scan
方法中的思想,让它更简洁、更通用、更可重用。我在我们的讨论中提到了这种技术,但 @jxck_
没有看到如何应用它。经过一番冗长的交流,由于语言障碍而受到一些阻碍,我问他是否可以借用他的笔记本电脑,通过键入一些代码来向他展示。
我定义了一个名为 errWriter
的对象,类似这样:
1 |
|
并给它一个方法 write.
,它不需要具有标准 Write
签名,并且部分采用小写以突出区别。 write
方法调用底层 Writer
的 Write
方法,并记录第一个错误以供将来参考:
1 |
|
一旦发生错误, write
方法将变为无操作,但会保存错误值。
给定 errWriter
类型及其 write
方法,可以重构上面的代码:
1 |
|
这更简洁,即使与使用闭包相比也是如此,而且还使页面上实际执行的写入序列更容易查看。不再有混乱了。使用错误值(和接口)进行编程使代码更简洁。
很可能同一软件包中的其他一些代码片段可以基于此想法进行构建,甚至直接使用 errWriter
。
此外,一旦存在 errWriter
,它还可以做更多的事情来提供帮助,尤其是在不太人工的示例中。它可以累积字节计数。它可以将写入合并到一个可以原子传输的单个缓冲区中。还有更多。
事实上,这种模式经常出现在标准库中。 archive/zip
和 net/http
包使用它。更重要的是, bufio
包的 Writer
实际上是 errWriter
思想的实现。虽然 bufio.Writer.Write
返回错误,但这主要与遵守 io.Writer
接口有关。 bufio.Writer
的 Write
方法的行为与我们上面的 errWriter.write
方法完全一样,其中 Flush
报告错误,因此我们的示例可以这样编写:
1 |
|
这种方法有一个明显的缺点,至少对于某些应用程序而言:无法知道在错误发生之前已完成了多少处理。如果该信息很重要,则需要更细粒度的处理方法。不过,通常情况下,最后进行全有或全无的检查就足够了。
我们只研究了一种避免重复错误处理代码的技术。请记住,使用 errWriter
或 bufio.Writer
并不是简化错误处理的唯一方法,这种方法并不适用于所有情况。然而,关键的经验教训是,错误是值,Go 编程语言的全部功能都可以用来处理它们。
使用该语言来简化错误处理。
但是请记住:无论您做什么,务必检查您的错误!