Errors are values [翻译]

2019-07-14 12:33发布

Errors are values

原文链接 - Rob Pike 对应github链接为 https://github.com/xuezhaojun/goyi/blob/master/errors%20are%20values.md csdn上文章发布后,不会再做修改,github上欢迎大家对错误或难以理解的地方批评指出 go 程序员,特别是刚接触 go 语言的go程序员,都会讨论到一个点 —- 如何处理 errors。 这些讨论往往会转化为对以下代码出现次数的哀叹: if err != nil { return err } 我们最近扫描了所有我们能找到的开源项目,并且发现这个代码一两页才出现一次,比大家想的要少的多。但是,如果还是有人坚持认为,他要敲很多的if err != nil的代码。那么一定是哪里出现了问题,而且很明显就是go本身。 好在这个问题很好纠正。事情可能是这样的:一个go语言的新人提出问题 “要怎么处理error呢?”,学会之后,就停在那里了。其他语言中,可能会需要 try-catch 或者其他机制来处理error。因此,程序员会想,之前什么时候用到 try-catch ,现在我go中就敲 if err != nil。慢慢的代码中就多出了很多这样的片段,结果显得笨笨的。 先不管这种解释是否合适,这些程序员没有一个关于errors的基本认知:errors are values,错误也是值。 可以对values编程,errors是values,那么你也可以对errors进行编程。 当然最常见的情况就是判断error的值是不是nil,但是其实你可以用error来做数不尽的事,稍加应用就可以明显改善程序,并消灭大部分死板的错误检查代码。 这里有一个bufio包中Scanner例子,它的Scan方法执行了底层IO,会导致一个error的产生,然而scan方法并没有返回一个error,而是返回了一个布尔值,和另一个在scan结束后调用的,分离的方法,来报告是否又error产生。客户端的代码如下: scanner := bufio.NewScanner(input) for scanner.Scan() { token := scanner.Text() // process token } if err := scanner.Err(); err != nil { // process the error } 这里有一处对于error的检查,但是它仅出现并执行一次!如果这个 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 error的时候,就会记录并返回false。另一个分离的方法 Err 会再客户端问询的时候反馈这个err。虽然看起来很微不足道,但是这样就和,到处敲if err != nil或者要求客户端每次获取token后都检擦错误,不一样了。 值得强调的是,不论设计如何,如果由error还是要检查一定要检查的。我们讨论的不是如何避免错误检查,而是如何应用这门语言优雅的错误检查。 重复的错误检查代码的议题,是在我参见2014年东京的GoCon大会出现的,另一个经验丰富的gopher也在他的twitter(用户名 @jxch_)中抱怨错误处理,他的示例代码如下: _, 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 } 我们可以通过借鉴之前Scan的方法,让代码更清晰,通用性更高。我之前在讨论中提到过这种技术,但是@jxch_并没有使用它。在语言阻碍造成的长时间沟通下,我询问是否我可以借用他的笔记本电脑,通过写一些代码来给他演示。 我定义了一个叫做 errWriter 的对象,如下: type errWriter struct { w io.Writer err error } 然后给它定义了一个方法 write(小写,为了和Write方法区分),这个write方法调用了Write方法,然后记录了err用作未来的引用。 func (ew *errWriter) write(buf []byte) { if ew.err != nil { return } _,ew.err = ew.w.Write(buf) } 一旦错误出现,write方法将直接返回,但是err会保存下来。 有了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 } 现在甚至比之前使用闭包还要清晰,也让代码流程种的write操作在页面上更可见。这下再没有笨笨的感觉了。使用error作为值编程让代码更棒了。 可能同一包中其他地方的代码也可以使用这种思想,甚至直接使用 errWriter。 同时,当 errWriter 存在,那么它可以做更多的事。特别是一些更实用的例子中,比如累计字节数,可以将合并写入到同一个buffer中,然后自动传送,等等。 实际上,这种模式也经常出现在标准库中,比如 archive/zipnet/http 中。bufio package’s Writer 就是完全实现了 errWriter 的思想。虽然bufio.Writer.Write会返回一个err,但是只是为了实现 io.Writer 的接口。bufio.Writer的Write方法和上面errWriter的表现一模一样,通过 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() } 这种方案的通病则是:再error出现之前,没法判断程序的完成度。如果这个信息很重要,那么则需要一个更加细粒度的方案,通常在末尾放一个 all or nothing 的错误检查就足够了。 我们现在只探讨了避免重复错误检查的一种手段。铭记errWriter这种方法并不是唯一的方法,并且这种方案也并不是适用于所有情况。关键在于,errors are values ,而go语言的强大之处就是你可以对其进行编程。 使用这门语言来简化你的错误处理吧。 但是记住,不管怎么做,一定要做错误检查! 最后,我和@jxck_交流的全部故事,包括他录制的一个小视频,可以在他的blog看到