os.ReadFile会爆内存是因为它一次性将整个文件加载到内存,适合小文件而非大文件;应改用流式处理如os.Open配合bufio.Scanner或io.Copy,并注意缓冲区大小、原子写入和错误处理。

为什么 os.ReadFile 会爆内存
直接用 os.ReadFile 读取几百 MB 或 GB 级文件,进程 RSS 内存几乎等于文件大小——Go 会一次性把全部内容加载进堆,还可能触发 GC 压力。这不是 bug,是设计使然:它本就面向小配置、JSON、模板等短文本场景。
- 替代方案必须流式处理:用
os.Open+bufio.Scanner(按行)或io.Copy(按块) - 注意
bufio.Scanner默认缓冲区仅 64KB,超长行会报scanner: token too long,需用Scanner.Buffer手动扩容 - 若需随机访问(如跳过前 N 字节),改用
*os.File的Seek方法,别硬塞进 Scanner
按块读写大文件的最小安全模式
最通用、内存可控的方式是固定缓冲区 + io.Copy 或手动 Read/Write 循环。关键不是“多大缓冲区”,而是“别让缓冲区成为瓶颈”。
- 推荐缓冲区大小:
32 * 1024(32KB)到1024 * 1024(1MB),实测在多数 SSD/HDD 上吞吐均衡 - 避免用
make([]byte, 0, size)反复切片——这仍会累积小对象;应复用sync.Pool或在循环外声明固定切片 - 写入时务必检查
Write返回的n, err:磁盘满、权限不足、断连都会在这里暴露,err == nil不代表写完
buf := make([]byte, 1<20) // 1MB
for {
n, err := src.Read(buf)
if n > 0 {
if _, writeErr := dst.Write(buf[:n]); writeErr != nil {
return writeErr
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}如何边读边处理(如 JSON 行、CSV、日志)
真正的大文件处理,90% 场景不需要全量加载——你要的是逐条解析、过滤、转换、聚合。这时核心是选对流式解析器,而非自己造轮子。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- JSON Lines(每行一个 JSON):用
json.Decoder配合bufio.Scanner,每次Decode一个对象,不缓存整行 - CSV:用标准库
encoding/csv.Reader,它内部已做缓冲和字段切分,设置Reader.FieldsPerRecord = -1允许变长列 - 纯文本日志:别用正则全局匹配;用
bufio.Scanner+ 自定义SplitFunc按时间戳或分隔符切分,减少字符串拷贝 - 注意:所有这些 Reader 都依赖底层
io.Reader,传入的必须是支持Read的句柄(如*os.File),别传bytes.Buffer后再转——又回去了
写入大文件时如何避免崩溃后数据损坏
直接 os.Create 写目标文件,程序 panic 或 kill -9 会导致文件截断或脏数据。生产环境必须加原子性保护。
立即学习“go语言免费学习笔记(深入)”;
- 标准做法:写入临时文件(如
out.json.tmp),写完调用os.Rename——在同分区下该操作是原子的 - 别用
os.Chmod或os.Chown改权限后再 rename:某些文件系统不保证 rename 后权限继承,应在 rename 后立即设置 - 如果目标路径跨设备(如从 /tmp 到 /data),
os.Rename会失败并退化为 copy+remove,此时需手动 fallback 并清理中间文件 - 最后记得
dst.Close():未 close 的文件句柄不会刷盘,os.Rename成功也不代表数据落盘
缓冲区大小、临时文件命名规则、错误分支的资源清理——这些细节不写进文档,但线上出问题时,八成卡在这三处。

















