Golang - Errors are values

AI 摘要: Go语言中,错误处理是将错误抽象为值的一种方式。本文介绍了一种避免重复错误处理代码的处理方式,即使用errWriter或bufio.Writer,但并不是适用于所有情况。

Go Error

在Go中,Errors are values

1. 抽象出来错误处理对比

Scan方法根本不会暴露错误。相反,它返回一个布尔值和一个单独的方法,在扫描结束时运行,报告是否发生了错误。

1
func (s *Scanner) Scan() (token []byte, error)

对比两段代码:

1
2
3
4
5
6
7
8
9
// good, 循环控制流没有检查错误,但它只出现并执行一次;循环直到完成,然后再担心错误
scanner := bufio.NewScanner(input)
for scanner.Scan() {
    token := scanner.Text()
    // process token
}
if err := scanner.Err(); err != nil {
    // process the error
}
1
2
3
4
5
6
7
8
9
// bad, 客户端必须在每次迭代时检查错误
scanner := bufio.NewScanner(input)
for {
    token, err := scanner.Scan()
    if err != nil {
        return err // or maybe break
    }
    // process token
}

这并没有太大的不同,但有一个重要的区别。在第二段代码中,客户端必须在每次迭代时检查错误,但在真正的Scanner API中,错误处理是从关键API元素抽象出来的,而关键API元素正在迭代令牌。

使用真正的API,客户端的代码因此感觉更自然:循环直到完成,然后担心错误。错误处理不会掩盖控制流。

2. 重复的错误处理

对比以下代码:

2.1. clips1:重复的错误处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 重复的错误处理
_, 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

2.2. clips2:关闭错误的函数

以下模式运行良好,但每个函数执行写操作需要一个闭包;

单独的辅助函数write()使用起来很笨,因为需要跨调用维护错误变量err(尝试它)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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
}

2.3. clips3:借用上面的Scan方法改进,借鉴这个想法并重复使用

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// 定义errWriter类型,用于写以及记录第一个错误
type errWriter struct {
    w   io.Writer
    err error
}

// 增加方法,write方法调用底层Writer的Write方法并记录第一个错误以供将来引用
// 一旦发生错误,write方法就会变为no-op,但会保存错误值ew.err。
func (ew *errWriter) write(buf []byte) {
    if ew.err != nil {
        return
    }
    _, ew.err = ew.w.Write(buf)
}

给定errWriter类型及其write方法,可以重构上面的代码:

1
2
3
4
5
6
7
8
ew := &errWriter{w: fd}
ew.write(p0[a:b]) //如果此时有错误,ew.err被赋值io.Writer.Write的写错误
ew.write(p1[c:d]) //继续调用ew.write将首先检测,ew.err,发现有错误终止
ew.write(p2[e:f]) //继续调用ew.write将首先检测,ew.err,发现有错误终止
// and so on
if ew.err != nil {
    return ew.err
}

改进后的一些特点:

  • 与使用闭包相比,clips3更清晰,并且还使得在页面上更容易看到实际的写入顺序。
  • 使用错误值(和接口)进行编程使代码更好。
  • 此外,一旦errWriter存在,它可以提供更多帮助,尤其是在真实的编程环境中;比如它可以累积字节数。它可以将写入合并到一个缓冲区中,然后可以原子方式传输。以及更多。

2.4. clips4: 类似的想法在标准库中有实现

实际上,这种模式通常出现在标准库中。 archive/zipnet/http包使用它。这个讨论更加突出,bufio包的Writer实际上是errWriter想法的实现。

虽然bufio.Writer.Write返回错误,但主要是关于尊重io.Writer接口。

bufio.Writer.Write方法就像我们上面的errWriter.write方法一样,Flush报告错误,因此我们的示例可以像这样编写:

1
2
3
4
5
6
7
8
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()
}

这种方法有一个明显的缺点,至少对于某些应用程序:在错误发生之前无法知道完成了多少处理。如果该信息很重要,则需要采用更细粒度的方法。但是,通常,最后的全有或全无检查就足够了

3. errWriter方式处理错误小结

我们只研究了一种避免重复错误处理代码的技术。请记住,使用errWriter或bufio.Writer并不是简化错误处理的唯一方法,并且这种方法并不适用于所有情况。

然而,文章内容关键点是:错误是值,并且Go语言可以适宜的处理它们。 但请记住:无论你做什么,总是检查你的错误!