直接将大文件全量读入内存再write会导致oom,正确做法是流式分块处理:用io.copy或手动read/write循环,buffer控制在32kb–1mb,复用同一byte切片,注意错误处理与sync策略。

为什么直接 Write 大文件会 OOM 或卡死
Go 的 os.File.Write 默认不缓冲,但更关键的是:如果你一次性把几 GB 数据读进 []byte 再传给 Write,内存直接爆掉。不是 IO 慢,是程序先被系统 kill —— 这是新手最常踩的坑。
真正要解决的不是“怎么写”,而是“怎么避免全量加载”。分块本质是流式处理:边读边写,内存只留一个 chunk。
- 典型错误:
data, _ := os.ReadFile("huge.bin")→ 立刻吃光 4GB 内存 - 正确姿势:用
io.Copy或手动Read/Write循环,chunk 大小控制在 32KB–1MB(太小增加 syscall 开销,太大占内存) - 注意
bufio.Writer对写入端有帮助,但对读取端无效;它不能替代分块逻辑,只是减少 write 系统调用次数
用 io.Copy + io.LimitReader 控制单次写入量
io.Copy 本身是流式复制,但默认不限长度。如果源是无限流或你只想写前 N 字节,得套一层 io.LimitReader。
比如导出数据库 dump 的前 100MB:
src, _ := os.Open("dump.sql")
dst, _ := os.Create("partial.sql")
defer dst.Close()
limited := io.LimitReader(src, 100*1024*1024)
_, err := io.Copy(dst, limited) // 自动按 32KB 分块
-
io.Copy内部使用 32KB buffer,无需手动管理 chunk slice - 若需自定义 chunk 大小(例如适配存储服务的分片要求),就不用
io.Copy,改用手动循环 - 注意
LimitReader到达 limit 后返回io.EOF,不是错误,别误判为读取失败
手动分块写入:控制 buffer 大小和错误处理
当你要精确控制每次写多少、或需要写入前做转换(如加密、压缩),就得手动分块。核心是复用同一块 []byte buffer,避免反复分配。
buf := make([]byte, 64*1024) // 复用 buffer
for {
n, err := src.Read(buf)
if n > 0 {
if _, werr := dst.Write(buf[:n]); werr != nil {
return werr // 不要忽略写入错误!磁盘满、权限问题都会在这里暴露
}
}
if err == io.EOF {
break
}
if err != nil {
return err
}
}
- buffer 大小建议 32KB–1MB:Linux 默认页大小 4KB,64KB 是常见折中值
- 必须检查
src.Read返回的n,不能直接Write(buf)—— 最后一次读可能只填了前 123 字节 -
dst.Write可能短写(尤其网络文件系统),生产环境应循环直到写完或出错,但本地磁盘通常不会短写,可简化
大文件分块写入时的 sync 和性能取舍
默认 os.File 写入走内核 page cache,断电可能丢数据。加 file.Sync() 能落盘,但慢 10–100 倍。是否调用,取决于场景。
- 日志类数据:可接受丢失最后几秒,不
Sync,靠os.O_SYNC打开文件更彻底 - 金融交易导出:每块写完必须
Sync,但 chunk 要够大(如 1MB),否则 syscall 太多 - 注意:
bufio.Writer的Flush()≠Sync();Flush()只清 buffer 到内核,不保证落盘 - 如果目标是对象存储(如 S3),分块写入逻辑完全不同——那是 multipart upload,要用
PutObjectPart,不是本地文件那一套
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











