逐行读取的坑

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

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最方便
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容

  • // bufio 包实现了带缓存的 I/O 操作 type Reader struct { ... } // Ne...
    佛心看世界阅读 1,677评论 0 0
  • 文章目录 文件信息接口os.FileInfo 文件的常规操作 (os包) 文件的读取和写入 (io及os包) io...
    __robin阅读 5,848评论 0 4
  • 官网 中文版本 好的网站 Content-type: text/htmlBASH Section: User ...
    不排版阅读 4,365评论 0 5
  • fmt格式化字符串 格式:%[旗标][宽度][.精度][arg索引]动词旗标有以下几种:+: 对于数值类型总是输出...
    皮皮v阅读 1,089评论 0 3
  • 甜甜今天进步了,学会了鸭鸭的发音,并且对上号了。 上午,甜甜和奶奶在床上玩,我也在旁边站着,听到甜甜偶然发出鸭鸭的...
    好读好书阅读 159评论 0 0