让我们编写一个函数来计算文件中的行数。

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 的操作来消除错误处理。

注意:
bufio.Scanner 可以扫描任何模式,但默认情况下它会查找换行符。

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

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

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

贴士:
当遇到难以忍受的错误处理时,请尝试将某些操作提取到辅助程序类型中。

最后编辑: kuteng  文档更新时间: 2021-01-09 21:50   作者:kuteng