go导出百万行excel内存爆2gb的主因是默认openfile全量加载dom树;应改用newstreamwriter流式写入,配合分页查询、每5000–10000行flush及defer f.close()确保文件完整。

Go 导出百万行数据时内存爆到 2GB?不是数据量太大,而是你没关掉“全量加载”开关——excelize 默认用 OpenFile + SetCell 就是 OOM 元凶;真要低内存,必须用 NewStreamWriter + 分页查 + 定期 Flush。
为什么用 OpenFile 写百万行必崩
这不是库的 bug,是设计使然:OpenFile 会把整个 XLSX 解压进内存构建 DOM 树,每写一行都缓存在内存里,直到 Save 才打包压缩。10 万行就可能吃掉 800MB+,百万行直接触发 GC 频繁停顿或 OOM。
- 错误示范:
f := excelize.OpenFile("out.xlsx"); f.SetCellValue("Sheet1", "A1", "x")—— 千万别在循环里这么干 - 真正流式写入只有一条路:
sw, _ := f.NewStreamWriter("Sheet1"),它不建 DOM,只生成 XML 片段并缓冲写入 -
SetRow比SetCellValue快 3 倍以上,因为避免了单单元格定位开销
NewStreamWriter 的三个关键操作时机
光用 StreamWriter 不够,不控制 Flush 频率,缓冲区照样堆满。它的内部 buffer 默认 4MB,但实际写入前全靠你手动触发刷新。
- 每写 5000–10000 行后调一次
sw.Flush():太频繁影响吞吐,太少撑爆 buffer - 写完表头立刻
sw.Flush():防止 header 被卡在 buffer 里,后续行错位 - 导出中途想中断?检查
ctx.Err() != nil后直接return,sw不会自动清理,但文件已部分写入(可用)
CSV 导出比 XLSX 省多少内存
不是“省一点”,是数量级差异:encoding/csv 写 CSV 基本不占额外内存,因为它就是往 io.Writer 里拼字符串,无解析、无样式、无压缩。100 万行 CSV 导出,常驻内存可压到 5MB 以内。
- Excel 打开乱码?不是库问题,是没加 BOM:
w.Write([]byte("\xef\xbb\xbf"))放最开头 - 字段含逗号/换行?
csv.Writer自动加双引号,但需确保字段是string类型,别传interface{}引发反射开销 - 别用
bytes.Buffer中转:直接io.Copy到http.ResponseWriter或文件句柄,避免中间拷贝
分页查询不配 context.WithTimeout 就是埋雷
导出接口挂起 5 分钟没人管?DB 连接池早被耗尽。分页本身不能防阻塞,必须配合超时和取消。
- 每次
db.Query都传ctx,不是只传给第一次查询 - 用
rows, err := stmt.QueryContext(ctx, offset),而不是Query—— 否则 cancel 无效 - offset 超过 10 万行?改用游标分页(
WHERE id > ?),否则 MySQLLIMIT 100000, 1000会扫 10 万行再丢弃
最容易被忽略的是:流式写入下,StreamWriter 的 Flush 不等于文件落盘,它只是把 buffer 推给底层 zip.Writer;最终 f.Close() 才真正封包。如果导出中途 panic,defer f.Close() 没执行,生成的 .xlsx 文件就是损坏的——务必在 sw.Flush() 后加日志或监控点,别只信 “写完了”。











