scanner卡住或oom因默认64kb缓冲不足,超长行触发扩容和gc压力;应调buffer设上限,超大行改用reader;writer需合理flush避免丢失或退化;scanner与reader勿混用同一源;缓冲区非越大越好,需按场景权衡。

Scanner 读大文件时卡住或 OOM,是因为没设缓冲区大小
默认 bufio.Scanner 的缓冲区只有 64KB,遇到超长行(比如日志里带大段 base64 或 JSON)直接报 scanner: token too long;更隐蔽的问题是,它内部用切片扩容,反复读取长行会触发大量内存分配,GC 压力陡增。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 用
bufio.NewScanner后立刻调Scaner.Buffer设上限,例如处理可能含万字日志行:scanner.Buffer(make([]byte, 64*1024), 10*1024*1024) - 真要读超大行(如单行 GB 级),别硬扛
Scanner,换bufio.Reader.ReadString('\n')或ReadBytes自己控缓冲和内存复用 - 注意
Buffer第二个参数是最大令牌长度,不是总读取量——它限制的是单次Scan()能吞下的字节数,不是文件总大小
Writer 写文件慢,多半是没启用或误用 Flush
bufio.Writer 不写满缓冲区就不会落盘,如果写完忘了 Flush(),数据卡在内存里,程序退出时可能丢失;但反过来,每写一行都 Flush(),等于退化成无缓冲 IO,性能比直接 os.Write 还差(多了函数调用和锁开销)。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 写文件结尾必须显式调
writer.Flush(),defer 不可靠——panic 时可能跳过 - 批量写结构化数据(如 CSV、JSON 行)时,按固定大小 flush,比如每 1MB 或每 1000 行后
Flush(),平衡延迟与内存占用 - 如果底层是网络连接(如 HTTP response writer),
Flush()会真正推数据出去,此时要注意客户端是否在等流式响应,别过早或过晚 flush
Scanner 和 Reader 混用导致丢数据,因为底层 reader 被 Scanner 预读了
常见场景:先用 scanner.Scan() 读几行,再想用 reader.ReadString('\n') 继续读——结果发现少了一行。原因是 Scanner 内部用了自己的 bufio.Reader,预读时已把后续数据从源 reader 提前拽进缓冲,原始 reader 的位置已经偏移。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 一个
io.Reader实例只交给一个缓冲器用,不要 scanner 和 reader 交替操作同一源 - 必须混用时,用
bufio.NewReader显式创建 reader,再把它的指针传给bufio.NewScanner,确保底层缓冲共享:r := bufio.NewReader(src); s := bufio.NewScanner(r) - 调试时可打印
r.Buffered()和r.ReadBytes('\n')看当前缓冲状态,验证是否丢数据
Bufio 缓冲大小设太大反而降低性能,尤其小文件或高频小写
缓冲区不是越大越好。设 1MB 缓冲写 1KB 文件,99% 是浪费;更麻烦的是,Go 的 bufio 在初始化时直接 make([]byte, size),大缓冲 = 大堆内存申请,GC 扫描压力上升;对 SSD 有写放大风险,对网络 socket 可能加剧 Nagle 算法延迟。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 文件 IO:小文件(
- 网络 IO:优先用系统默认(通常 4KB),除非明确测出 packet 合并收益;HTTP server 等高并发场景慎调,buffer 太大会挤占 goroutine 栈空间
- 永远用
runtime.ReadMemStats对比不同 buffer 下的AllocBytes和PauseNs,别猜
缓冲区是个开关,不是油门——开太小卡住,开太大憋着,中间那个点得靠真实数据打出来。











