大家好,我是炸鱼。前段时间分享了一篇文章《10+ 条 Go 官方谚语,你知道几条?》,引发了很多小伙伴的讨论。其中之一是“错误就是价值”。大家在“错误就是价值观”或者“错误就是价值观”之间来回跳转,不容易纠缠不清。其实说这句话的罗布·派克是用一篇文章《Errors are values》来诠释这句谚语的意思的。它到底是什么?今天,我就和大家一起学做炸鱼吧。下面的“我”都代表罗伯·派克。背景Go程序员(尤其是刚接触该语言的程序员)经常讨论的问题之一是如何处理错误。对于下面代码片段出现的次数,谈话往往变成感叹(各大平台吐槽批评多,认为设计不好)。下面的代码:iferr!=nil{returnerr}扫描代码片段我们最近扫描了我们能找到的每一个Go开源项目,发现这段代码片段每两页只出现一次,这比某些人想象的要少。尽管如此,如果人们仍然认为他们必须不断地键入代码,例如:iferr!=nil那么肯定是哪里出了问题,而明显的目标是Go语言本身(设计糟糕?)。误解显然,这是不幸的、误导性的,而且很容易纠正。也许现在刚接触Go的程序员会问,“我如何处理错误?”,学习模式,然后就此打住。在其他语言中,可能会使用try-catch块或其他类似机制来处理错误。所以程序员认为在Go中我只需要输入iferr!=nil而我在以前的语言中会使用try-catch。随着时间的推移,Go代码收集了太多这样的片段,结果感觉很笨拙。错误是值无论这种解释是否恰当,很明显这些Go程序员忽略了关于错误的一个基本观点:错误是值。值是可以编程的,既然错误是值,错误也可以被编程。当然,涉及错误值的常见语句是测试它是否为nil,但是您可以使用错误值做无数其他事情,应用其中一些其他事情可以使您的程序变得更好,从而消除大量样板.如果使用死记硬背的if语句检查每个错误(又名iferr!=nil无处不在),就会出现这种情况。bufio示例下面是bufio包中Scanner类型的一个简单示例。它的Scan方法执行低级I/O,这当然会导致错误。但是,Scan方法根本不会暴露错误。相反,它返回一个布尔值,并在扫描结束时运行一个单独的方法来报告是否发生错误。客户端代码如下所示:err!=nil{//processtheerror}当然有nil校验错误,但只出现并执行一次。Scan方法可以改为定义为:func(s*Scanner)Scan()(token[]byte,error)然后,用户代码的示例可以是(取决于如何检索令牌):scanner:=bufio.NewScanner(input)for{token,err:=scanner.Scan()iferr!=nil{returnerr//ormaybebreak}//processtoken}这没有太大区别,但有一个重要区别。在此代码中,客户端必须在每次迭代时检查错误,但在真正的ScannerAPI中,错误处理是从迭代令牌的关键API元素中抽象出来的。使用真正的API,客户端代码因此感觉更自然:循环直到完成,然后担心错误。错误处理不会混淆控制流。当然,幕后发生的事情是,一旦Scan遇到I/O错误,它会记录它并返回false。当客户端询问时,一个单独的方法Err报告错误值。虽然这是微不足道的,但它与在每次iferr!=nil之后放置或要求客户端检查错误不同。这是使用错误的值进行编程。简单的编程,是的,但仍然是编程。值得强调的是,无论设计如何,程序检查错误都是至关重要的,无论错误暴露在何处。这里讨论的不是关于如何避免检查错误,而是关于使用语言优雅地处理错误。实践讨论当我在东京参加2014年秋季的GoCon时,出现了复制错误检查代码的话题。一位热情的Gopher,在Twitter上被称为@jxck\_,回应了关于错误检查的熟悉的哀叹。他有一些代码,从结构上看是这样的:_,err=fd.Write(p0[a:b])iferr!=nil{returnerr}_,err=fd.Write(p1[c:d])iferr!=nil{returnerr}_,err=fd.Write(p2[e:f])iferr!=nil{returnerr}//如此重复。在实际代码中,这段代码更长并且有更多的事情要做,所以仅仅用一个辅助函数重构这段代码并不容易,但在这种理想化的形式中,一个函数字面意义上的closes对错误变量很有用:varerrerrorwrite:=func(buf[]byte){iferr!=nil{return}_,err=w.Write(buf)}write(p0[a:b])write(p1[c:d])write(p2[e:f])//等等iferr!=nil{returnerr}这种模式运行良好,但需要在执行写入的每个函数中关闭;由于需要在调用之间维护err变量(请尝试一下),因此辅助函数for的使用起来很笨拙。我们可以借用上面扫描方式的思想,让它更简洁、更通用、更易复用。我在我们的讨论中提到了这项技术,但@jxck\_没有看到如何应用它。交流了半天,我问能不能借他的笔记本电脑敲一些代码给他看。我这样定义了一个名为errWriter的对象:typeerrWriterstruct{wio.Writererrerror}并给它一个方法,Write。它不需要具有标准的Write签名,并且为了区分而部分小写。write方法调用底层Writer的Write方法,记录第一个错误以供参考:func(ew*errWriter)write(buf[]byte){ifew.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])//依此类推ifew.err!=nil{returnew.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])//等等ifb.Flush()!=nil{returnb.Flush()}这个方法有一个明显的不利的一面是,至少对于某些应用程序而言:无法知道在错误发生之前完成了多少处理。如果该信息很重要,则需要更细粒度的方法。不过,通常情况下,最后进行全有或全无检查就足够了。总结在本文中,我们只研究了一种避免错误处理代码重复的技术。请记住,使用errWriter或bufio.Writer并不是简化错误处理的唯一方法,而且并非在所有情况下都有效。然而,关键的教训是错误是值,并且可以使用Go编程语言的全部功能来处理它们。使用这种语言来简化错误处理。但请记住:无论你做什么,都要检查你的错误!文章持续更新中。可以微信搜索【脑补炸鱼】阅读。本文已收录在GitHubgithub.com/eddycjy/blog中。学习Go语言可以看Go学习地图和路线。欢迎星星提醒。Go书系列Go语言入门系列:初探Go项目实战Go语言编程之旅:深入使用Go做项目Go语言设计哲学:理解Go的Why与设计思维Go语言进阶之旅:走得更远GoSourceCode阅读更多想添加箭头语法,这更像PHP!Go错误处理的新思路?使用左手函数和表达式
