写大文件应避免将全部数据加载进内存,推荐流式写入:用bufio.NewWriterSize控制缓冲区大小,或io.CopyBuffer配合sync.Pool复用缓冲区,禁用bytes.Buffer全量接收。

直接用 os.WriteFile 或裸 io.Copy 写大文件,内存占用会随文件大小线性飙升——这不是你代码写错了,而是默认行为把整块数据塞进堆里等 GC 回收。关键不是“怎么写”,而是“怎么不把数据全加载进来”。
别用 bytes.Buffer 接大文件内容
这是最常踩的坑:哪怕只是中转,bytes.Buffer 也会在内部不断 grow,每次扩容都触发内存分配 + 数据拷贝 + GC 压力。100MB 文件可能让 RSS 涨到 300MB+。
- 绝对不要
buf := &bytes.Buffer{}; io.Copy(buf, src)再os.WriteFile(..., buf.Bytes(), ...) - 如果必须用
bytes.Buffer,务必预分配容量:buf := bytes.NewBuffer(make([]byte, 0, 100<code>1024</code>1024))(但仅限你能预估上限的场景) - 更推荐直接流式写入:src → dst,中间不落地为内存对象
用 bufio.NewWriterSize 控制缓冲区大小
裸 os.File.Write 每次调用都是一次系统调用;bufio.Writer 把小写合并成大块,但缓冲区设太大反而浪费内存、拖慢响应。
- 写入吞吐为主(如日志归档、CSV 导出):用
bufio.NewWriterSize(f, 64<code>1024</code>1024)(64KB) - 写入延迟敏感(如实时转发):降到
8<code>1024</code>1024(8KB),避免 buffer 积压 - 务必在写完后调用
wr.Flush(),否则数据卡在 buffer 里,后续读取不可见 - 不要对同一个
*os.File套多个bufio.Writer,文件偏移错乱
用 io.CopyBuffer + sync.Pool 复用读写缓冲
io.Copy 内部硬编码了 32KB 临时切片,每次调用都 new 一次——高频写入(如并发上传)下 GC 飙升。
立即学习“go语言免费学习笔记(深入)”;
- 定义全局复用池:
var bufPool = sync.Pool{New: func() any { return make([]byte, 0, 64<code>1024</code>1024) }} - 使用时:
buf := bufPool.Get().([]byte); io.CopyBuffer(dst, src, buf[:0]); bufPool.Put(buf) - 注意:
buf[:0]是关键,复用底层数组但清空逻辑长度;传[64<code>1024</code>1024]byte{}会每次新建数组,更差 - 该模式特别适合:HTTP 中转、S3 上传、多文件批量写入
写入目标决定是否落盘,别无脑先存本地
很多服务把上传文件先 os.CreateTemp 写一遍,再读出来发给对象存储——这等于双倍 IO + 双倍内存暂存(page cache + Go runtime buffer)。
- 纯中转(如代理上传到 S3):直接
io.Copy(dstWriter, partBody),dst 是s3.PutObjectInput.Body或http.Post的 body - 需要校验/转码/抽帧:才用
os.CreateTemp落盘,写完立刻file.Close(),后续操作用新os.Open打开 - 切忌用
bytes.Buffer接整个文件再.Bytes()—— 它不支持 flush 到文件,且 grow 不可控
真正卡住内存的往往不是单次写入逻辑,而是缓冲区生命周期管理:没 Flush、没复用、没及时关闭句柄、或误把流式场景当成内存加载场景来处理。这些点漏掉一个,1GB 文件就可能吃掉 2GB RSS。


















