这是我参与11月更文挑战的第2天,活动详情查看:2021最后一次更文挑战
导语
在任何一种编程语言中,对文件操作都是必不可少的一部分,甚至在广义上,Unix系统提出了一切设备皆文件,几乎涵盖了操作系统级别的所有IO操作。在学习go语言时同样需要了解go语言是如何处理普通文件操作的。幸运的是go标准库提供了足够丰富的文件操作工具,而且通过这些工具的实现代码,也可以学习到一些文件操作的高质量代码的写法。值得指出的是go标准库代码质量非常高,不仅仅文件操作,还有非常多的内容值得深入挖掘,感兴趣的小伙伴已经可以自己前往挖宝了~
有一个重要的文件读取场景是按照行逐行读取并处理文件,这样的文件一般是特别大的文件,不能一次性都读到内存中,只能分块或分段的处理,下边我们看一下应该如何写出正确的高质量的逐行读取文件的代码。
按行读取文件
一般网文代码#1如下:
func ReadFile(filePath string) error {
f, err := os.Open(filePath)
defer f.Close()
if err != nil {
return err
}
buf := bufio.NewReader(f)
for {
line, err := buf.ReadLine("\n")
if err != nil {
if err == io.EOF{
return nil
}
return err
}
// handle here
return nil
}
}
注意:该代码片段中有两处问题,此时你可以停下来看看自己是否可以找得到问题所在。
更严格的说,还可能有另外一处问题存在,我们在下边统一分析。
以下代码#2来自csv标准库。简单说明一下csv文件格式的要领,csv文件按行存储,每一行代表一组数据,该组数据包含多个字段,不同字段默认用逗号分割。我们来分析一下csv标准库是如何按行读取数据的。
// readLine reads the next line (with the trailing endline).
// If EOF is hit without a trailing endline, it will be omitted.
// If some bytes were read, then the error is never io.EOF.
// The result is only valid until the next call to readLine.
func (r *Reader) readLine() ([]byte, error) {
line, err := r.r.ReadSlice('\n')
if err == bufio.ErrBufferFull {
r.rawBuffer = append(r.rawBuffer[:0], line...)
for err == bufio.ErrBufferFull {
line, err = r.r.ReadSlice('\n')
r.rawBuffer = append(r.rawBuffer, line...)
}
line = r.rawBuffer
}
if len(line) > 0 && err == io.EOF {
err = nil
// For backwards compatibility, drop trailing \r before EOF.
if line[len(line)-1] == '\r' {
line = line[:len(line)-1]
}
}
// 其他代码略
}
// NewReader returns a new Reader that reads from r.
func NewReader(r io.Reader) *Reader {
return &Reader{
Comma: ',',
r: bufio.NewReader(r),
}
}
这两个代码片段都通过bufio标准库实现了按行读取文件,代码#1中第10行错误的使用了bufio底层函数ReadLine,该函数虽然是导出函数,但是并不适合上层应用直接调用,从其注释说明中也可见一斑。
// ReadLine is a low-level line-reading primitive. Most callers should use
// ReadBytes('\n') or ReadString('\n') instead or use a Scanner.
// ReadLine是一个底层按行读取原语。大部分调用者应该使用ReadBytes('\n') 或 ReadString('\n'),或者扫描器
注意:网上有大量代码示例是无视该说明文档的,潜在的风险则是代码panic后可能会造成数据的损失。
代码#1中另一个问题是对EOF处理不当,虽然判断了EOF错误,但是却没有正确处理读到EOF时的数据,代码片段#2正确处理了。这种场景会发生在文件的最后一行而该行并没有换行符,如果用代码#1就会漏掉该行数据。
上边我们提到代码#1严格的说还有另外一个问题就是对缓存满的错误没有处理,直接就报错退出了,这样的错误处理是无效的,将该错误返回给上层甚至使用者都无助于解决这个错误,这是一个不能靠重试就可以解决的错误。总不能要求他们改一下待处理的数据,我们的程序缓存不够处理不了,这就贻笑大方了。
从两段代码对比我们可以看出,高质量的代码是高度关注细节,高度准确的,这些细节往往体现在对边界场景和错误的处理上。
按照csv标准库修改后端代码#1如下:
func ReadFile(filePath string) error {
f, err := os.Open(filePath)
defer f.Close()
if err != nil {
return err
}
buf := bufio.NewReader(f)
for {
line, err := buf.ReadString('\n')
if err != nil && err != io.EOF{
return err
}
if len(line) > 0 {
}
// handle here
return nil
}
}
注意:bufio的ReadString或ReadBytes函数内部已经处理了缓冲区的错误,故只需要处理EOF错误即可。