bufio.scanner默认64kb单行上限,超长即panic;需用scanner.buffer()设合理上限(如1mb),避免math.maxint32致oom;长期保存行内容须深拷贝,否则被后续scan()覆盖。

bufio.Scanner 是 Go 中逐行读取大文件的默认首选,但它不是开箱即用就安全的——默认单行上限 64KB,超长就直接 panic: scanner: token too long。真正可控、低内存占用的方案,取决于你是否需要保留行内容、行长度是否可预期、以及是否要处理边界情况。
为什么 bufio.Scanner 默认会 panic?
它不是“读一行分配一次内存”,而是先在内部缓冲区里攒够一整行(直到遇到 \n 或 \r\n),再把这一段切出来返回。缓冲区默认只有 64KB,一旦某行(比如嵌入 base64 的日志、单行 JSON)超过这个长度,就触发 panic。
- 这不是 bug,是设计取舍:牺牲极端情况的健壮性,换流式处理的简洁和复用效率
-
scanner.Text()返回的字符串底层指向 Scanner 自己的缓冲区,下一次Scan()就会覆盖——若你把它存进[]string或map却没深拷贝,最后所有值都变成最后一行 - 调用
scanner.Buffer(make([]byte, 0, 1MB), 1MB)可抬上限,但别设成math.MaxInt32:畸形输入(如全文件无换行)会导致 OOM
什么时候该换用 bufio.Reader.ReadString('\n')?
当你需要明确控制内存峰值、处理非标准换行(如 \r)、或必须保留末行(文件不以换行结尾)时,bufio.Reader 更透明。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
ReadString('\n')每次都 new 一个新字符串,GC 压力比Scanner.Text()高,但语义清晰:你拿到的就是独立字符串,不怕被覆盖 - 初始化时也建议指定缓冲大小:
bufio.NewReaderSize(file, 1MB),避免小缓冲导致系统调用频繁 - 注意错误判断:除了
io.EOF,还要检查err != nil && !errors.Is(err, io.EOF),因为ReadString在 EOF 且缓冲区无完整行时返回io.ErrUnexpectedEOF
如何避免内存泄漏?关键不在“怎么读”,而在“读完怎么扔”
Go 的 GC 不会立刻回收,真正吃内存的是你无意中持有的引用。
- 别把
scanner.Text()直接塞进全局map[string]int或闭包里——除非你确认后续不会再修改那块缓冲区 - 需要暂存多行?用
line := append([]byte(nil), scanner.Bytes()...)或strings.Clone(scanner.Text())(Go 1.20+)做深拷贝 - 处理完一行后,显式置空变量:
line = "",有助于 GC 尽早识别可回收对象 - 用
runtime.ReadMemStats定期采样,观察Alloc和HeapInuse是否随文件增长而线性上升——如果是,大概率有隐式引用没断
行内解析 JSON 时的额外开销陷阱
如果每行是完整 JSON 对象(JSON Lines 格式),别用 json.Unmarshal([]byte(line), &v)。
-
[]byte(line)会复制一次字节,json.Unmarshal内部再复制——两倍冗余拷贝 - 改用
json.NewDecoder(strings.NewReader(line)).Decode(&v),它直接在字符串底层字节上解析,零额外分配 - 更进一步:如果行内容稳定、字段少,考虑用
gjson.Get(line, "field").String()跳过结构体解码,省去反射开销
bufio.Scanner,只要你在某个 map 里存了它的指针,或者忘了清空切片,几 GB 的日志照样能让你的进程在 P99 延迟上突然卡住。










