对于错误,Go 不采取异常处理机制,而是透过返回error
值来表示是否发生了什么错误,最基本的做法就是:
if err != nil {
// 做些什么
}
然而,接触 Go 不用多久就会发现,若要认真地检查、处理错误,if err != nil
之类的代码就会到处充斥,特别是在进行 IO 之类的操作时更是如此,单纯地if err != nil
写法最后会写到怀疑人生,这么写真的是对的吗?
这时可能会做的选择之一是:就别检查了吧!如果写的是特定目的之程序、不太需要考虑太多状况、不用考虑过多的稳固性、想要很快地写出原型之类的,这个选择可能是正确的,毕竟真要认真写 Go 中的错误检查,某些程度上就像 Java 中常被人嫌的受检异常(Checked exception)一样啰嗦,还好 Go 可以选择不检查…XD
只不过,如果想写出较通用、具有稳固性的程序,错误检查就是必需的,Go 也鼓励开发者积极地检查错误;那么…干脆全panic
好了?
func check(err) {
if err != nil {
panic(err)
}
}
这么一来,遇到要检查错误时,就调用check
来检查,这样就能少写些if err != nil
了吧!这种做法其实并不建议,因为panic
是panic
,error
是error
,panic
的场合,应该用在适用panic
的场合,也就是那些实际上真的无法处理的错误,发生这类错误最重要的引发开发者恐慌,让开发者知道要修改程序的演算,避免发生panic
。
panic
就像 Java 中发生RuntimeException
,其实不建议捕捉,而是停下程序,修正演算上的错误。
不过,可以想想为什么会有人想在发生错误时,一律引发panic
,因为可以从目前的执行处中断,就像异常处理机制中异常发生时,后续代码就不会执行那样。
这就是以检查是否有错误的方式,没办法直接做到的事,因为不在检查出错误的时候进行return
、break
之类的动作,代码就会往下执行。
为了能在错误发生时中断流程,就有可能写出这类的代码:
_, 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
}
// 诸如此类
这段代码摘自〈Errors are values〉,该文章中提到一个解决的方式是:
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
}
这么一来,每一次write
调用时,就都会检查err
是否为nil
,如果不是nil
就return
,实际上也就不会执行w.Write
,虽然代码上调用了write
多次;然而,某次调用若发生了错误,后续的write
并不会真正执行写出的动作,而透过这个方式,可以将发生错误时要进行的动作,统整在最后检查并执行。
匿名函数的方式创建了 Closure,捕捉了err
变量,这么一来就得做些回避同名变量的问题,另外匿名函数的写法也不是那么简明,因此文章中定义了:
type errWriter struct {
w io.Writer
err error
}
func (ew *errWriter) write(buf []byte) {
if ew.err != nil {
return
}
_, ew.err = ew.w.Write(buf)
}
这么一来,每个io.Writer
可以有个别的err
可以使用,而原本的程序就可以改写为:
ew := &errWriter{w: fd}
ew.write(p0[a:b])
ew.write(p1[c:d])
ew.write(p2[e:f])
// 诸如此类
if ew.err != nil {
return ew.err
}
在〈bufio 包〉中看过的bufio.Writer
就是这类的设计:
type Writer struct {
err error
buf []byte
n int
wr io.Writer
}
...略
func (b *Writer) Write(p []byte) (nn int, err error) {
for len(p) > b.Available() && b.err == nil {
var n int
if b.Buffered() == 0 {
// Large write, empty buffer.
// Write directly from p to avoid copy.
n, b.err = b.wr.Write(p)
} else {
n = copy(b.buf[b.n:], p)
b.n += n
b.Flush()
}
nn += n
p = p[n:]
}
if b.err != nil {
return nn, b.err
}
n := copy(b.buf[b.n:], p)
b.n += n
nn += n
return nn, nil
}
... 略
func (b *Writer) Flush() error {
if b.err != nil {
return b.err
}
if b.n == 0 {
return nil
}
n, err := b.wr.Write(b.buf[0:b.n])
if n < b.n && err == nil {
err = io.ErrShortWrite
}
if err != nil {
if n > 0 && n < b.n {
copy(b.buf[0:b.n-n], b.buf[n:b.n])
}
b.n -= n
b.err = err
return err
}
b.n = 0
return nil
}
在b.err
不为nil
的情况下,实际上不会有实际的写出,而Flush
时,若b.err
不为nil
就会被return
,因此在使用bufio.Writer
时,可以如下编写,在最后检查
b := bufio.NewWriter(fd)
b.Write(p0[a:b])
b.Write(p1[c:d])
b.Write(p2[e:f])
// 诸如此类
if b.Flush() != nil {
return b.Flush()
}
这个模式可以进一步应用,例如在〈bufio 包〉中看过bufio.Scanner
的使用,语义上比较高阶:
scanner := bufio.NewScanner(f)
for scanner.Scan() {
fmt.Println(scanner.Text())
}
if err := scanner.Err(); err != nil {
panic(err)
}
scanner.Scan()
返回布尔值,表示是否扫描到下一行,没有下一行或中途发生错误,就会返回false
;然而循环检查就只在乎有没有下一行,离开循环后再来检查错误,两个程序区块各司其职。
bufio.Scanner
本身的组成中有io.Reader
与err
:
type Scanner struct {
r io.Reader
...略
err error
...略
}
若你查看Scan
方法的实现,会返回false
的情况之一,就是Scanner
的err
不是nil
:
...略
if s.err != nil {
// Shut it down.
s.start = 0
s.end = 0
return false
}
...略
Go 不以特定语法处理错误(例如 Java 使用try..catch
),正因为错误发生是返回错误,也就会有许多方式可以检查错误,这边只是谈到几个可用的设计,重点在于观察代码的需求,适时地重构,看看如何以设计的方式,优雅地处理错误,而不是避免检查错误,如果一开始没什么方向,可以多观察 Go 程序库的源码实现中是怎么处理错误的。