让我们编写一个函数来计算文件中的行数。
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.EOF
。ReadString
需要某种方式在没有什么可读时来停止。因此,在我们将错误返回给 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
,而不会遇到其他错误。
贴士:
当遇到难以忍受的错误处理时,请尝试将某些操作提取到辅助程序类型中。