www.zhblog.net

err 是否 nil?


对于错误,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了吧!这种做法其实并不建议,因为panicpanicerrorerrorpanic的场合,应该用在适用panic的场合,也就是那些实际上真的无法处理的错误,发生这类错误最重要的引发开发者恐慌,让开发者知道要修改程序的演算,避免发生panic

panic就像 Java 中发生RuntimeException,其实不建议捕捉,而是停下程序,修正演算上的错误。

不过,可以想想为什么会有人想在发生错误时,一律引发panic,因为可以从目前的执行处中断,就像异常处理机制中异常发生时,后续代码就不会执行那样。

这就是以检查是否有错误的方式,没办法直接做到的事,因为不在检查出错误的时候进行returnbreak之类的动作,代码就会往下执行。

为了能在错误发生时中断流程,就有可能写出这类的代码:

_, 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,如果不是nilreturn,实际上也就不会执行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.Readererr

type Scanner struct {
    r            io.Reader 
    ...略
    err          error
    ...略
}

若你查看Scan方法的实现,会返回false的情况之一,就是Scannererr不是nil

...略
    if s.err != nil {
        // Shut it down.
        s.start = 0
        s.end = 0
        return false
    }
    ...略

Go 不以特定语法处理错误(例如 Java 使用try..catch),正因为错误发生是返回错误,也就会有许多方式可以检查错误,这边只是谈到几个可用的设计,重点在于观察代码的需求,适时地重构,看看如何以设计的方式,优雅地处理错误,而不是避免检查错误,如果一开始没什么方向,可以多观察 Go 程序库的源码实现中是怎么处理错误的。


展开阅读全文

评论

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 心情