Go的Error - 错误即值 (Errors are values)

 提示:转载请注明原文链接

 本文永久链接:https://www.360us.net/article/34.html

文章是翻译自Go官方博客的文章:http://blog.golang.org/errors-are-values

随便翻译的一下,给自己参考,有需要的同学也可以参考参考,翻译的不好的地方或者难以理解的地方请参考原文。

本人英语本身也不怎么样!所有有可能会有理解错误,用词不当,语言组织不当的地方,请见谅!



该怎么样去处理错误(errors),是go程序员特别是刚学go的人之间的一个共同讨论点。

讨论经常会随着下面这段程序出现的次数越来越多而变成抱怨。

if err != nil {
    return err
}

我们最近扫描了所有我们能够找到的开源项目,发现重复出现这段代码的次数是每页或者每两页只出现了一次,是不是比你想象中的要少很多。

然而,如果你还是感觉必须到处写if err != nil,那一定是哪里出了问题,并且会认为问题很明显是出在go自己身上。


很不幸,这是错误的,而且这很容易去纠正。

假如一个新手go程序员去提问这发生了什么,“怎么会只有一个错误处理?”,其实你应该学习这种模式,并且保持它。

在其他编程语言里面,一种可能方式是使用try-catch块或者其他类似原理的方式去处理错误(errors)。

因此,程序员会想,当我在别的语言里面使用try-catch的时候,go只需要输入if err != nil,随着时间的推移,这样的片段会越来越多,结果就是感觉很糟糕。


抛开这些描述,很明显,这些go程序员忽略了关于错误的基本要点:错误也是值(Errors are values)。


值(values)可以程序化(programmed),因为错误(errors)也是值,所以错误也可以程序化(programmed)。


一个普遍的描述包括测试一个错误值是否为空(nil),但是还有很多其他使用方式,使用其中的某些东西可以使你的程序更好,可以很大程度上排除用固定模式的if语句去检查错误。


这里有一个包bufio Scanner类型的简单示例。

Scan方法执行基础的I/O,它可以引发一个错误,当然,Scan方法不会返回错误,它返回一个boolean值。

但是有一个单独的方法会在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),然后用户可能会这样写代码:

scanner := bufio.NewScanner(input)
for {
    token, err := scanner.Scan()
    if err != nil {
        return err // or maybe break
    }
    // process token
}

和上面这段代码是很不一样的,但是还有一个很重要的区别。

在这段代码里面,每次循环都要检测错误,但是在实际的Scanner API,错误处理是从API元素的键抽象出来的,迭代的时候通过token传递。

使用实际的API,代码因此会感觉更自然:循环执行完再检测错误。错误处理并不能覆盖流动控制。


幕后发生了什么呢?当然,当Scan一遇到I/O错误,它会记录错误并返回false。

一个独立的方法Err,当你调用时,它会返回错误值。

虽然这是微不足道的,但是和到处写if err != nil并且在每个token后面检测错误还是不同的。

这就是用错误值编程设计。无论怎么样,是的,就是这个简单的设计。


不管这个设计的价值,不管怎样检测错误都是至关重要的。

这里的论点不是关于怎么样去避免错误检测,而是关于怎样用go去优雅的处理错误。


我参加在东京的2014年秋GoCon时,重复出现的错误检测代码是我的主题。他是一个Twitter名叫jxck_的狂热gopher,对于重复的错误检测代码也深恶痛绝。

他展示了一些代码:

_, 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 function)来重构是不容易的。

不过在理想情况下,使用一个闭包(匿名函数)来传递错误变量将会有帮助:

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变量在调用的时候需要保持。


通过借鉴上面Scan方法的思路,我们可以使它更干净,优雅和可复用。我们上面探讨的这个方法jxck_不知道怎么去使用。

因为有点语言上的障碍,在一个长时间的交流之后,我问是否可以借他的笔记本电脑给我,然后给他展示一些代码。


我定义了一个叫errWriter的对象,代码如下:

type errWriter struct {
    w   io.Writer
    err error
}

再给它定义一个write方法。它不需要有一个标准的Write定义,并且还有个突出的区别是它是小写的。

write方法调用底层WriterWrite方法,记录第一个错误,以备查询:

func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

如果一有错误发生,write方法什么都不干,只保存错误值。

基于errWriterwrite方法,上面的代码可以被重构:

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,会有比它所做的更多的帮助,特别是不是人为的例子。

它可以计算字节数,可以合并多个输入到当个buffer,并且可以自动传输。还有更多用处。


事实上,这种模式通常出现在标准库里面。archive/zipnet/http都使用了它。

更突出的点是,bufio包的Writer实际上就是errWriter思路的一个实现。

bufio.Writer.Write返回一个错误,这主要是因为io.Writer接口的原因。

bufio.WriterWrite方法的行为正如我们上面的errWeiter.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语言完全有能力去处理他们。


使用这个语言去简化你的错误处理。


但是请记住:无论你做什么,请总是要去检测错误。


最后,@jxck_的完整信息,包括一小段视频记录,请访问他的博客


By Rob Pike


 评论
暂无评论