- 7.1.1. 计算行数
7.1.1. 计算行数
让我们编写一个函数来计算文件中的行数。
func CountLines(r io.Reader) (int, error) {var (br = bufio.NewReader(r)lines interr 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.EOF。ReadString 需要某种方式在没有什么可读时来停止。因此,在我们将错误返回给 CountLine 的调用者之前,我们需要检查错误是否是 io.EOF,如果不是将其错误返回,否则我们返回 nil 说一切正常。
我认为这是 Russ Cox 观察到错误处理可能会模糊函数操作的一个很好的例子。我们来看一个改进的版本。
func CountLines(r io.Reader) (int, error) {sc := bufio.NewScanner(r)lines := 0for 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,而不会遇到其他错误。
贴士:当遇到难以忍受的错误处理时,请尝试将某些操作提取到辅助程序类型中。
