来源:cyningsun.github.io/08-19-2019/…html
Go程序员,尤为是那些刚接触语言的人,常见的讨论点是如何处理错误。 谈话常常变成对如下代码段出现次数的失望git
if err != nil {
return err
}
复制代码
咱们最近扫描了咱们能够找到的全部开源项目,发现这个代码段每一页或每两页只发生一次,比大家想象的更少。 尽管如此,若是必须老是写程序员
if err != nuil
复制代码
的感受持续存在, 必定是出了什么问题,明显的目标就是 Go
自己。github
这是使人遗憾和误导性的,并且很容易纠正。事实可能正是Go
新程序员想问的:“如何处理错误?”,他们碰到这种模式,而后停在那里。在其余语言中,可使用 try-catch
块或其余此类机制来处理错误。所以,程序员认为,当我使用旧语言的 try-catch
时,在 Go
中我只需输入 if err != nil
。随着时间的推移,Go
代码聚集了许多这样的片断,结果显得很笨拙。golang
先无论这种解释是否合适,很明显这些 Go
程序员缺乏关于错误的一个根本点: Errors are values
。编程
值能够编程,既然错误是值,所以错误也能够编程。闭包
固然,涉及错误值的常见语句是检测它是否为nil,可是还有无数其余能够用错误值作的事情,而且应用其中的一些东西可使您的程序变得更好,从而消除大量若是机械的使用if语句检查每一个错误会出现的样板。less
如下是 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
}
复制代码
固然,有出现错误的空值检查,但它只出现并执行一次。 能够将 Scan
方法定义为函数
func (s *Scanner) Scan() (token []byte, error) 复制代码
而后示例用户代码多是(取决于如何取回 token),
scanner := bufio.NewScanner(input)
for {
token, err := scanner.Scan()
if err != nil {
return err // or maybe break
}
// process token
}
复制代码
并无太大的不一样,但有一个重要的区别。 在此代码中,客户端必须在每次迭代时检查错误,但在真正的 Scanner
API 中,错误处理从关键 API 元素抽象出来,而关键 API 元素正在迭代 token。 使用真正的 API,客户端的代码更天然:循环直到完成,最后进行错误处理。错误处理不会掩盖控制流。
固然,幕后是,只要 Scan
遇到 I/O 错误,它就会记录它并返回 false。 一个单独的 Err
方法 在客户端调用时报告错误值。 虽然很微不足道,但它与处处敲
if err != nil
复制代码
或要求客户端在每一个 token 以后检查错误不一样。它正在用错误值编程。简洁的编程,对,仍仍是编程。
值得强调的是,不管设计如何,程序检查错误都是相当重要的。这里的讨论不是关于如何避免检查错误,而是关于使用语言,优雅的处理错误。
当我参加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
复制代码
代码重复性很高。 在实际代码中,会更长,还有更多内容,所以使用 helper 函数重构它并不容易,但在如此理想化的状况下,封装错误变量的函数字面值会有用:
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
}
复制代码
该模式颇有效,但每一个执行写操做的函数都须要一个闭包; 单独的 helper 函数使用起来比较笨拙,由于 err 变量须要跨调用维护(试试看)。
经过借鉴上述 Scan
方法的想法,咱们可使代码更清洁,更通用和可重复使用 。我在讨论中提到过这种技术,但 @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
并非简化错误处理的惟一方法,而且这种方法并不适合全部状况。 然而,关键的一课是 errors are values
,而且Go编程语言的所有功能可用于处理它们。
使用语言简化错误处理。
但请记住:不管你怎么作,必定要检查本身的错误!
最后,关于我与 @jxck 互动的完整故事,包括他录制的一个小视频,请访问他的博客 。