共计 3691 个字符,预计需要花费 10 分钟才能阅读完成。
大家好,我是煎鱼。
前段时间我分享了一篇文章《10+ 条 Go 官网谚语,你晓得几条?》,引发了许多小伙伴的探讨。其中有一条“Errors are values”,大家在是“谬误是值”还是“谬误就是价值”中重复横跳,纠结不易。
其实说这句话的 Rob Pike,他用一篇文章《Errors are values》诠释了这句谚语的意思,到底是什么?
明天煎鱼和大家一起学习,以下的“我”均代表 Rob Pike。
背景
Go 程序员,尤其是那些刚接触该语言的程序员,常常探讨的一个问题是如何处理错误。对于以下代码片段呈现的次数,谈话常常变成悲叹(各大平台吐槽、批评十分多,认为设计的不好)。
如下代码:
if err != nil {return err}
扫描代码片段
咱们最近扫描了咱们能找到的所有 Go 开源我的项目,发现这个代码片段只在每一两页呈现一次,比一些人认为的要少。
尽管如此,如果人们依然认为必须常常输出如下代码:
if err != nil
那么肯定有什么中央出了问题,而显著的指标就是 Go 语言自身(说设计的不好?)。
谬误的了解
显然这是可怜的,误导的,而且很容易纠正。兴许当初的状况是,刚接触 Go 的程序员会问:” 如何处理错误?”,学习这种模式,而后就此打住。
在其余语言中,人们可能会应用 try-catch 块或其余相似机制来处理错误。因而,程序员认为,当我在以前的语言中会应用 try-catch 时,我在 Go 中只需输出 if err != nil。
随着工夫的推移,Go 代码中收集了许多这样的片段,后果感觉很蠢笨。
谬误是值
不论这种解释是否适合,很显著,这些 Go 程序员错过了对于谬误的一个基本点:谬误是值(Errors are values)。
值能够被编程,既然谬误是值,那么谬误也能够被编程。
当然,波及谬误值的常见语句是测试它是否为 nil,然而还有有数其余事件能够用谬误值做,并且利用其中一些其余事件能够使您的程序更好,打消很多样板。
如果应用死记硬背的 if 语句查看每个谬误,就会呈现这种状况(也就是 if err != nil 到处都是的状况)。
bufio 例子
上面是一个来自 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,客户端的代码因而感觉更天然:循环直到实现,而后放心谬误。
错误处理不会覆盖管制流程。
当然,在幕后产生的事件是,一旦 Scan 遇到 I/O 谬误,它就会记录它并返回 false。当客户端询问时,一个独自的办法 Err 会报告谬误值。
尽管这很微不足道,但它与在每个 if err != nil
后到处放或要求客户端查看谬误是不一样的。这是用谬误值编程。简略的编程,是的,但依然是编程。
值得强调的是,无论设计如何,程序查看谬误是至关重要的,无论它们裸露在哪里。这里的探讨不是对于如何防止查看谬误,而是对于应用语言优雅地处理错误。
实战探讨
当我加入在东京举办的 2014 年秋季 GoCon 时,呈现了反复谬误查看代码的话题。一位热心的 Gopher,在 Twitter 上的名字是 @jxck\_,回应了咱们相熟的对于谬误查看的悲叹。
他有一些代码,从构造上看是这样的:
_, 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}
这种模式成果很好,但须要在每个执行写入的函数中敞开;独自的辅助函数应用起来比拟蠢笨,因为须要在调用之间保护 err 变量(尝试一下)。
咱们能够通过借用下面的扫描办法的思路,使之更简洁、更通用、更可重复使用。我在咱们的探讨中提到了这个技术,然而 @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])
// and so on
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])
// and so on
if b.Flush() != nil {return b.Flush()
}
这种办法有一个显著的毛病,至多对于某些应用程序而言:没有方法晓得在谬误产生之前实现了多少解决。如果该信息很重要,则须要更细粒度的办法。不过,通常状况下,最初进行全有或全无查看就足够了。
总结
在本文中咱们只钻研了一种防止反复错误处理代码的技术。
请记住,应用 errWriter 或 bufio.Writer 并不是简化错误处理的惟一办法,而且这种办法并不适用于所有状况。
然而,要害的教训是谬误是值,Go 编程语言的全副性能可用于解决它们。
应用该语言来简化您的错误处理。
但请记住:无论您做什么,都要查看您的谬误!
文章继续更新,能够微信搜【脑子进煎鱼了】浏览,本文 GitHub github.com/eddycjy/blog 已收录,学习 Go 语言能够看 Go 学习地图和路线,欢送 Star 催更。
Go 图书系列
- Go 语言入门系列:初探 Go 我的项目实战
- Go 语言编程之旅:深刻用 Go 做我的项目
- Go 语言设计哲学:理解 Go 的为什么和设计思考
- Go 语言进阶之旅:进一步深刻 Go 源码
更多浏览
- Go 想要加个箭头语法,这下更像 PHP 了!
- Go 错误处理新思路?用左侧函数和表达式