结论:用bufio.Scanner读超大文件危险,因默认64KB单行限制会panic,且无分块能力;应改用bufio.Reader+固定大小缓冲区或io.ReadFull手动分块读取。

直接说结论:用 bufio.Scanner 读超大文件是危险的,默认会因单行过长 panic,它不是为“分块”设计的,而是为“按行扫描”设计的;真要处理 GB 级文件,得换 bufio.Reader + 手动切片,或用 io.ReadFull 控制缓冲区。
为什么 bufio.Scanner 不适合超大文件分块读取
很多人看到“Scanner 支持缓冲”就默认它能分块,但它的底层逻辑是:每次调用 Scan() 都尝试读到下一个分隔符(默认换行),并把整行内容加载进内存。一旦某行超过 MaxScanTokenSize(默认 64KB),就会直接 panic:scanner: token too long —— 这在日志、CSV 或二进制混合文本里极常见。
-
Scanner没有“读 N 字节”的接口,Bytes()和Text()返回的是当前行全部内容,无法控制粒度 - 无法跳过前 N 字节、无法随机定位、无法复用底层
Reader的缓冲区 - 即使调大
Split函数自定义分隔符,仍受限于单次分配上限,且性能随行长度波动剧烈
真正可控的分块读法:用 bufio.Reader + 固定大小 []byte 缓冲区
这才是面向超大文件的正解:手动管理缓冲区大小,逐块读取,不依赖行结构,内存占用恒定。
核心思路是:开一个固定大小的 []byte(比如 1MB),反复调用 reader.Read(buf),它返回本次实际读到的字节数,读完自动推进偏移。
立即学习“go语言免费学习笔记(深入)”;
func readInChunks(filename string, chunkSize int) error {
f, err := os.Open(filename)
if err != nil {
return err
}
defer f.Close()
reader := bufio.NewReader(f)
buf := make([]byte, chunkSize)
for {
n, err := reader.Read(buf)
if n > 0 {
// 处理 buf[:n],注意不是整个 buf
processChunk(buf[:n])
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
return nil
}
- 必须用
buf[:n],否则可能处理到上一轮残留数据 -
chunkSize建议设为 2^10 ~ 2^20(1KB–1MB),太小导致系统调用频繁,太大增加 GC 压力 - 如果文件含 UTF-8 多字节字符且需按文本边界切分,不能简单按字节切,得用
utf8.DecodeRune向后对齐,否则会截断字符
遇到带结构的超大文件(如 JSON 行、CSV)怎么办
这类场景不能硬切字节块,否则会把一条记录劈成两半。正确做法是:用 bufio.Reader 配合状态机,边读边识别完整记录边界。
- JSON Lines(每行一个 JSON 对象):用
reader.ReadString('\n')替代Read,它会自动等待换行符,内部仍走缓冲,不会一次全读入 - CSV(含换行符的字段):必须用
csv.Reader,它内部已处理引号转义和跨行字段,且支持Read()单行返回[]string,底层复用bufio.Reader - 如果自己解析协议(如自定义二进制头+体),建议用
binary.Read配合io.LimitReader控制单次读取上限,避免越界
最常被忽略的一点:无论用哪种 Reader,打开文件后记得检查 os.Stat().Size(),预估总块数有助于做进度提示或并发分片;另外,Linux 下超大文件建议用 os.O_DIRECT(需对齐)绕过页缓存,但 Go 标准库不直接支持,得用 syscall,普通场景没必要折腾。


















