你知道有什么比改进错误处理的语法更好吗?那就是根本不需要处理错误。

本节从 John Ousterhout 最近的著作“软件设计哲学”[9]中汲取灵感。该书的其中一章是“定义不存在的错误”。我们将尝试将此建议应用于 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
}

由于我们遵循前面部分的建议,CountLines 需要一个 io.Reader,而不是一个 *File;它的任务是调用者为我们想要计算的内容提供 io.Reader

我们构造一个 bufio.Reader,然后在一个循环中调用 ReadString 方法,递增计数器直到我们到达文件的末尾,然后我们返回读取的行数。

至少这是我们想要编写的代码,但是这个函数由于需要错误处理而变得更加复杂。 例如,有这样一个奇怪的结构:

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

我们在检查错误之前增加了行数,这样做看起来很奇怪。

我们必须以这种方式编写它的原因是,如果在遇到换行符之前就读到文件结束,则 ReadString 将返回错误。如果文件中没有换行符,同样会出现这种情况。

为了解决这个问题,我们重新排列逻辑增来加行数,然后查看是否需要退出循环。

但是我们还没有完成检查错误。当 ReadString 到达文件末尾时,预期它会返回 io.EOFReadString 需要某种方式在没有什么可读时来停止。因此,在我们将错误返回给 CountLine 的调用者之前,我们需要检查错误是否是 io.EOF,如果不是将其错误返回,否则我们返回 nil 说一切正常。

我认为这是 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() 方法退出循环,我们就可以获取该错误。

最后, sc.Err() 负责处理 io.EOF 并在达到文件末尾时将其转换为 nil,而不会遇到其他错误。

WriteResponse

我的第二个例子受到了 Errors are values 博客文章[10]的启发。

在本章前面我们已经看过处理打开、写入和关闭文件的示例。错误处理是存在的,但是接收范围内的,因为操作可以封装在诸如 ioutil.ReadFileioutil.WriteFile 之类的辅助程序中。但是,在处理底层网络协议时,有必要使用 I/O 原始的错误处理来直接构建响应,这样就可能会变得重复。看一下构建 HTTP 响应的 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 的单个返回值。

这里很多重复性的工作。 我们可以通过引入一个包装器类型 errWriter 来使其更容易。

errWriter 实现 io.Writer 接口,因此可用于包装现有的 io.WritererrWriter 写入传递给其底层 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
}

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 的两个返回值。

02-20 10:12