• 7.1.1. 计算行数

    7.1.1. 计算行数

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

    1. func CountLines(r io.Reader) (int, error) {
    2. var (
    3. br = bufio.NewReader(r)
    4. lines int
    5. err error
    6. )
    7. for {
    8. _, err = br.ReadString('\n')
    9. lines++
    10. if err != nil {
    11. break
    12. }
    13. }
    14. if err != io.EOF {
    15. return 0, err
    16. }
    17. return lines, nil
    18. }

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

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

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

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

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

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

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

    注意:这个逻辑仍然不完美,你能发现错误吗?

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

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

    1. func CountLines(r io.Reader) (int, error) {
    2. sc := bufio.NewScanner(r)
    3. lines := 0
    4. for sc.Scan() {
    5. lines++
    6. }
    7. return lines, sc.Err()
    8. }

    这个改进的版本从 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,而不会遇到其他错误。

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