通过消除错误消除错误处理

本文译自 Eliminate error handling by eliminating errors 版权@归原文所有.

Go 2 旨在改善错误处理的开销, 但是你知道有什么比处理错误的改进语法更好吗? 根本不需要处理错误. 现在, 我不是说 “删除你的错误处理代码”, 相反我建议改变你的代码, 这样你就没有多少错误需要处理.

本文从 John Ousterhout 软件设计的哲学的一章中汲取灵感, “定义不存在的错误”. 我尝试将他的建议应用于 Go.

这是一个计算文件行数的函数:

func CountLines(r io.Reader) (int, error) {
        var (
                br    = bufio.NewReader(r)
                lines int
                err   error
        )

        for {
                _, err = br.ReadString('\n')
                lines++
                if err != nil {
                        break
                }
        }

        if err != io.EOF {
                return 0, err
        }
        return lines, nil
 }

我们构造一个 bufio.Reader, 然后在一个循环中调用 ReadString 方法, 递增计数器直到我们到达文件的末尾, 然后我们返回读取的行数. 这是我们编写的代码, 而 CountLines 因错误处理而变得更加复杂.

例如, 有这种奇怪的结构:

 _, err = br.ReadString('\n')
lines++
if err != nil {
        break
}

我们在检查错误之前增加行数, 这看起来很奇怪. 我们必须以这种方式编写它的原因是, 如果在遇到换行符之前遇到文件结尾-io.EOF, 则 ReadString 将返回错误. 如果没有尾换行符, 则会发生这种情况.

为了定位这个问题, 我们重整逻辑以增加行数, 然后查看是否需要退出循环.

但是我们还没有完成错误检查. 当 ReadString 到达文件末尾时, 它将返回 io.EOF. 这是预期的, ReadString 需要某种方式来终止, 没有更多内容需要读取. 所以在我们将错误返回给 CountLine 的调用者之前, 我们需要检查错误是不是 io.EOF, 并且在这种情况下将其传递, 否则我们返回 nil 表示一切正常. 这就是为什么函数的最后一行不是简单的:

return lines, err

我认为这是 Russ Cox 错误的处理可能会模​​糊函数操作的一个很好的例子. 我们来看一个改进的版本:

func CountLines(r io.Reader) (int, error) {
        sc := bufio.NewScanner(r)
        lines := 0

        for sc.Scan() {
                lines++
        }

        return lines, sc.Err()
}

这个改进的版本从使用 bufio.Reader 切换到 bufio.Scanner. 本质上 bufio.Scanner 使用 bufio.Reader 添加一层抽象, 这有助于消除掩盖我们之前版本的 CountLines 的操作的错误处理.

如果扫描器匹配了一行文本并且没有遇到错误, 则 sc.Scan() 方法返回 true. 因此, 只有当扫描器的缓冲区中有一行文本时, 才会调用 for 循环体. 这意味着我们修改后的 CountLines 正确处理了没有尾换行符的情况, 它还正确处理了文件为空的情况.

其次, 当 sc.Scan 遇到错误时返回 false, 当到达文件结尾或遇到错误时, 我们的 for 循环将退出. bufio.Scanner 类型会记住遇到的第一个错误, 一旦我们使用 sc.Err() 方法退出循环, 我们就恢复该错误.

最后, buffo.Scanner 负责处理 io.EOF 并在到文件末尾时将其转换为 nil, 而不会遇到其他错误.

我的第二个例子的灵感来自于 Rob Pike 的错误是值的博客文章.

在处理打开, 写入和关闭文件时, 错误处理存在但不是压倒性的, 因为操作可以封装在诸如 ioutil.ReadFile 和 ioutil.WriteFile 之类的帮助程序中. 但是, 在处理低级网络协议时, 通常需要使用 I/O 原语直接构建响应, 因此错误处理可能变得重复. 考虑构建 HTTP/1.1 响应的 HTTP 服务器的这个片段:

type Header struct {
        Key, Value string
}

type Status struct {
        Code   int
        Reason string
}

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
        _, err := fmt.Fprintf(w, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)
        if err != nil {
                return err
        }

        for _, h := range headers {
                _, err := fmt.Fprintf(w, "%s: %s\r\n", h.Key, h.Value)
                if err != nil {
                        return err
                }
        }

        if _, err := fmt.Fprint(w, "\r\n"); err != nil {
                return err
        }

        _, err = io.Copy(w, body)
        return err
}

首先, 我们使用 fmt.Fprintf 构造状态行, 并检查错误. 然后对于每个头, 我们写入键和值, 每次都检查错误. 最后, 我们使用额外的 \r\n 终止头部分, 检查错误, 并将响应体复制到客户端. 最后, 虽然我们不需要检查来自 io.Copy 的错误, 但我们需要将它从 io.Copy 返回的两个返回值形式转换为 WriteResponse 期望的单个返回值.

这不仅是大量的重复工作, 每个操作 - 从根本上将字节写入 io.Writer - 具有不同形式的错误处理. 但是我们可以通过引入一个小包装类型来变得容易:

type errWriter struct {
        io.Writer
        err error
}

func (e *errWriter) Write(buf []byte) (int, error) {
        if e.err != nil {
                return 0, e.err
        }

        var n int
        n, e.err = e.Writer.Write(buf)
        return n, nil
}

errWriter 实现了 io.Writer 接口, 所以可以用它来包装现有的 io.Writer. errWriter 将写入传递给其底层写入器(writer), 直到检测到错误. 从那起, 它会丢弃任何写入并返回先前的错误.

func WriteResponse(w io.Writer, st Status, headers []Header, body io.Reader) error {
        ew := &errWriter{Writer: w}
        fmt.Fprintf(ew, "HTTP/1.1 %d %s\r\n", st.Code, st.Reason)

        for _, h := range headers {
                fmt.Fprintf(ew, "%s: %s\r\n", h.Key, h.Value)
        }

        fmt.Fprint(ew, "\r\n")
        io.Copy(ew, body)

        return ew.err
}

将 errWriter 应用于 WriteResponse 可以显著提高代码的清晰度. 每个操作不再依附在一个错误处理块. 通过检查 ew.err 字段, 将错误报告移动到函数的末尾, 避免从 io.Copy 的返回值进行恼人的转换.

当你发现自己面临恼人的错误处理时, 请尝试将某些操作提取到帮助程序类型中.

comments powered by Disqus

2019-02-06


On this page: