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/zip
和net/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语言可以适宜的处理它们。 但请记住:无论你做什么,总是检查你的错误!