这个系列主要记录一些在开发中踩的坑,方便日后查询
逐行读取的坑
当需要逐行读取文件的时候,大致的代码可能是这样的
func main() {
scanner := bufio.NewScanner(io.Reader)
for scanner.Scan() {
line := scanner.Text()
}
}
复制代码
这里io.Reader指的是输入源
但是,最近在开发中遇到一个问题,按照上述的方式实现后,莫名其面的出现了文件没有读取完就退出,且过程中没有报任何错误;重新review了代码,并没有发现问题,怀疑是bufio.Scanner
的问题,然后重新读了一下Go的文档,发现了官网的example:
scanner := bufio.NewScanner(os.Stdin)
for scanner.Scan() {
fmt.Println(scanner.Text()) // Println will add back the final '\n'
}
if err := scanner.Err(); err != nil {
fmt.Fprintln(os.Stderr, "reading standard input:", err)
}
复制代码
原来scanner
还有一个Err()
方法,官方的说法是Err returns the first non-EOF error that was encountered by the Scanner.
,也就是说如果scanner.Scan()
如果出错,错误信息是要通过Err()
方法才能得到的,我的go程序将这个Err忽略了,代码补充完整之后看到这样的错误:bufio.Scanner: token too long
遂查看Scan()
源码,发现以下逻辑
// Is the buffer full? If so, resize.
if s.end == len(s.buf) {
// Guarantee no overflow in the multiplication below.
const maxInt = int(^uint(0) >> 1)
if len(s.buf) >= s.maxTokenSize || len(s.buf) > maxInt/2 {
s.setErr(ErrTooLong)
return false
}
newSize := len(s.buf) * 2
if newSize == 0 {
newSize = startBufSize
}
if newSize > s.maxTokenSize {
newSize = s.maxTokenSize
}
newBuf := make([]byte, newSize)
copy(newBuf, s.buf[s.start:s.end])
s.buf = newBuf
s.end -= s.start
s.start = 0
}
复制代码
原来Scanner
在初始化的时候有设置一个maxTokenSize
,这个值默认是MaxScanTokenSize = 64 * 1024
,当一行的长度大于64*1024
即65536
之后,就会出现ErrTooLong
这个错误
同时在官方文档中发现:
Scanning stops unrecoverably at EOF, the first I/O error, or a token too large to fit in the buffer. When a scan stops, the reader may have advanced arbitrarily far past the last token. Programs that need more control over error handling or large tokens, or must run sequential scans on a reader, should use bufio.Reader instead.
大致意思是如果token太大(行太长)的情况下,要使用bufio.Reader
,但是bufio.Reader
就没有这个问题了么?
看了以下bufio.Reader
的代码,发现也是有缓冲区大小限制的,并且默认缓冲区大小是4096
,不过有一个函数NewReaderSize
可以调整这个缓冲区大小。
查看bufio.Reader
提供的ReadLine()
函数
ReadLine is a low-level line-reading primitive. Most callers should use ReadBytes('\n') or ReadString('\n') instead or use a Scanner.
大致意思是这个函数比较底层,建议使用ReadBytes('\n')
或ReadString('\n')
或Scanner
,继续看说明
ReadLine tries to return a single line, not including the end-of-line bytes.If the line was too long for the buffer then isPrefix is set and the beginning of the line is returned. The rest of the line will be returned from future calls. isPrefix will be false when returning the last fragment of the line. The returned buffer is only valid until the next call to ReadLine. ReadLine either returns a non-nil line or it returns an error,never both.
从这里我们能看出来设置缓冲区的作用了,ReadLine
会尽量去读取并返回完整的一行,但是如果行太长缓冲区满了的话,就不会返回完整的一行而是返回缓冲区里面的内容,并且会设置isPrefix
为true
;这时候需要继续调用ReadLine
直到将完整一行读完,然后外层调用程序需要将这些块拼起来才能组成完整的行。不仅要处理isPrefix
,还要处理前缀,太麻烦!除非我们主动设置缓冲区大小,但是前提是你必须知道最长行的长度,大多数情况下这个是无法提前预知的。怪不得建议使用ReadBytes
或ReadString
或者Scanner
。
遂看ReadBytes
和ReadString
的源码,发现ReadString
是调用的ReadBytes
,且ReadBytes
已经将缓冲区大小的问题解决了
for {
var e error
frag, e = b.ReadSlice(delim)
if e == nil { // got final fragment
break
}
if e != ErrBufferFull { // unexpected error
err = e
break
}
// Make a copy of the buffer.
buf := make([]byte, len(frag))
copy(buf, frag)
full = append(full, buf)
}
复制代码
用起来比ReadLine
要方便,但是坑在于一定要做好分隔符的标示,如果你的文件文件中写的是"a\nb\nc\nd",使用ReadString
的时候会漏掉d
的输出,但是在最后一个line
里面还有有数据的,所以需要自己手动的判断一下
以上的总结我们得出:
Scanner
中的单行大小限制是65536bufio.Reader
缓冲区大小是4096- 如果数据没有特殊情况下,逐行读取使用
ReadString
最方便
有疑问加站长微信联系(非本文作者)