zoukankan      html  css  js  c++  java
  • golang如何优雅的处理错误

    错误是值 Errors are values

    原文地址

    Rob Pike
    12 January 2015

    在程序员中,尤其是go新手,经常听到的一个讨论话题是:如何处理错误。当下面这段代码出现次数过多时,这个话题大多数时候都会变成对go的悲叹。

    if err != nil {
        return err
    }
    

    我们最近扫描了所有我们能找到的开源项目代码,但是确发现这段代码平均一两页出现一次,远没有我们原本设想的出现次数那么多。如果有人任然坚信一定要输入if err !=nil,那么肯定是那里出错了,显然,是go本身的问题。

    这是不幸,有误,并且容易纠正的一个问题。可能当go新手在询问”如何处理错误“的时候,他们学会了这个模式(指if err !=nil),并且对错误的认知到此为止了。在其他语言,一个程序员使用try-catch代码块或者而其他类似的机制去处理错误。因此,这些程序员们认为,在我之前的语言中我用try-catch去处理错误,那么我在go中我也要用相应的if err != nil处理错误就行了。随着时间过去,在go代码中出现许多这样的小片段,导致看起来十分愚蠢。

    无论这个解释再怎么好,很明显的一点就是go程序员忽略了一个关于错误的概念:错误是值(Errors are value)。

    我们可以对值进行编程,并且因为错误是值,所以我们也可以对错误进行编程。

    原文:Values can be programmed, and since errors are values, errors can be programmed.

    一个常见的涉及到错误的语句是测试错误值是不是空值,但是我们还可以对错误值进行无数种处理,使用其中一些处理,可以让我们的程序变得更好,消灭那些每一错误都要用样板if处理的语句。

    这里有一个简单例子,来自bufio包的Scanner类,它的Scan方法执行基础的I/O,这其中明显可能产生错误,然而,scan方法并没有一个错误,而是返回一个布尔值,并且在scan方法执行结束后,运行一个单独的方法,报告是否单独发生错误。客户端代码如下

    scanner := bufio.NewScanner(input)
    for scanner.Scan() {
        token := scanner.Text()
        // process token
    }
    if err := scanner.Err(); err != nil {
        // process the error
    }
    

    当然,可以对每个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
    }
    

    这两个没有多大的不同,但是有一个很重要的区别是,在后者的客户端代码,用户必须在每次循环中都检查一次错误,但是在真正的ScannerAPI中,只会对token进行迭代,错误处理从关键API代码中抽象出来了。所以在真实的API中,用户端的代码因此更加自然,一直循环直到结束,然后再考虑错误,错误处理并不会影响到控制流。

    在这背后发生了什么呢?当scan遇到了I/O错误,它会记录并且返回false,另一个单独的方法Err,报告这个错误当客户端调用它的时候,虽然这有些琐碎,但是不同于到处输入if err != nil,或者告诉客户必须时刻检查每个token是否有错误。这个方法对错误进行了编程。简单的编程处理,仍然是编程。

    值得强调的是,无论使用哪种设计,最至关重要的是,无论错误如何暴露出来,都要去检查处理它。我们在这的讨论,不是讨论如何避免检查错误,而是如何使用go更优雅地处理错误。

    我在东京参加2014年秋季GoCon大会时,反复出现了错误检查代码的话题。一个热情的 gopher,@jxck,反映了他对错误处理的抱怨,他有一些像这样的代码:

    _, 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
    

    这出现了过多重复的代码。在真实的编码过程中,它更长些,并且有更多情况要处理,因此用一个辅助函数去重构它并不容易,但是在理想环境中,定义一个函数来处理错误变量或许能帮助我们

    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
    }
    

    这个方式看起来不错,但是在执行写操作的时候,每个函数都需要闭包操作,每次单独的辅助函数调用的时候都需要维护一个共同变量。

    我们可以做得更干脆点,更适用点,并且复用性更高,通过借鉴上文的Scan方法,我向@jxck提到了这个技巧,但是他不知道如何去使用,在一段时间的交流后,由于语言障碍,我借用他的电脑,写下了这样一段代码,试着去提醒他。

    我定义了一个对象叫做errWriter,如下:

    type errWriter struct {
        w   io.Writer
        err error
    }
    

    并且给他赋予一个方法write,他并不需要标准的Write方法签名,并且他的小写突出了区别。写方法调用了Write方法,并且记录了第一个遇到的错误用于后续使用。

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

    当错误发生时,write将会变成一个无操作函数,但是会保存错误值。

    定义errWriter类型,并且他的write方法,上述代码可以被重构为

    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的存在,还可以做更多的事情来提供帮助,特别是在不那么人工的例子中,他可以积累字节数,可以将写操作合并到一个缓冲区中,以原子的方式操作。

    事实上,这个方式经常出现在标准包汇总。archive/zipnet/http 都有使用到这个方法。本文的讨论更为突出的表现了 bufio package's Writer 就是基于errWriter想法实现的,虽然 bufio.Writer.Write 返回一个错误,这主要是基于io.Writer接口。Writer方法的实现就像我们上文写的errWriter.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 编程语言的全部功能可用于处理它们。

    使用该语言简化错误处理。

    但是请记住:无论做什么,都要检查错误!

  • 相关阅读:
    Django创建模型。
    Kubernetes APIService资源
    应用系统管理风险
    [Golang] go list命令查看依赖的版本
    [Golang] 升级gin框架和jwtgo
    [Git] 解决git错误 OpenSSL SSL_read: Connection was reset, errno 10054
    [Github] 配置ssh免密码登录解决 You've successfully authenticated, but GitHub does not provide shell access.
    go循环遍历小坑
    Go 字符串拼接6种,最快的方式 strings.builder
    uniapp安卓真机调试提示检测不到手机【解决办法】
  • 原文地址:https://www.cnblogs.com/Jun10ng/p/12787840.html
Copyright © 2011-2022 走看看