golang踩坑实录(一)

蛋挞先生 · · 1062 次点击 · · 开始浏览    
这是一个创建于 的文章,其中的信息可能已经有所发展或是发生改变。

这个系列主要记录一些在开发中踩的坑,方便日后查询

逐行读取的坑

当需要逐行读取文件的时候,大致的代码可能是这样的

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*102465536之后,就会出现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会尽量去读取并返回完整的一行,但是如果行太长缓冲区满了的话,就不会返回完整的一行而是返回缓冲区里面的内容,并且会设置isPrefixtrue;这时候需要继续调用ReadLine直到将完整一行读完,然后外层调用程序需要将这些块拼起来才能组成完整的行。不仅要处理isPrefix,还要处理前缀,太麻烦!除非我们主动设置缓冲区大小,但是前提是你必须知道最长行的长度,大多数情况下这个是无法提前预知的。怪不得建议使用ReadBytesReadString或者Scanner

遂看ReadBytesReadString的源码,发现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中的单行大小限制是65536
  • bufio.Reader缓冲区大小是4096
  • 如果数据没有特殊情况下,逐行读取使用ReadString最方便

有疑问加站长微信联系(非本文作者)

本文来自:掘金

感谢作者:蛋挞先生

查看原文:golang踩坑实录(一)

入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889

1062 次点击  
加入收藏 微博
暂无回复
添加一条新回复 (您需要 登录 后才能回复 没有账号 ?)
  • 请尽量让自己的回复能够对别人有帮助
  • 支持 Markdown 格式, **粗体**、~~删除线~~、`单行代码`
  • 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
  • 图片支持拖拽、截图粘贴等方式上传