Gin导出Excel内存爆掉的根源在于未分页查询、非流式写入及同步响应,须采用游标分页+ SXSSFWorkbook flushRows流式写入+异步任务三件套。

直接上结论:Gin 本身不处理 Excel 写入,内存爆掉的锅不在 Gin,而在你用的 Excel 库和数据库读取方式。真正要动刀的地方是「分页查询 + 流式写入 + 异步响应」三件套,缺一不可。
为什么Gin里导出Excel容易OOM
很多人以为加个 gin.Context.Writer 就完事了,结果百万行一跑,服务直接卡死。根本原因有两个:
- 数据库查询没分页,
db.Find(&list)把几百万条全 load 到内存,Go 的 slice 会连续分配大块堆内存,GC 都来不及回收 - Excel 库用了
XSSFWorkbook或未配置滑动窗口的SXSSFWorkbook,每个Row和Cell都是独立对象,100 万行 × 20 列 ≈ 2000 万个对象,内存轻松破 1.5GB
必须用游标分页替代 OFFSET/LIMIT
OFFSET 在大数据量下性能断崖式下跌,比如 OFFSET 1000000,MySQL 还得扫前 100 万行。Gin 后端该用基于主键或时间戳的游标分页:
- 前端传
last_id=123456(非页码),后端查WHERE id > ? ORDER BY id LIMIT 1000 - 避免
SELECT *,只查导出需要的字段,减少网络和内存开销 - MyBatis 或 GORM 查询时显式设置
QueryContext的fetchSize(如用sqlx可设Rows.Next()批量拉取)
流式写入必须配 SXSSFWorkbook + flushRows
别碰 XSSFWorkbook,它把整个文件结构存在内存;也别只 new 一个 SXSSFWorkbook 就完事——没调 flushRows(),照样 OOM:
- 初始化时指定窗口大小:
workbook := xlsx.NewSXSSFWorkbook(500)(保留 500 行在内存) - 每写满一批(比如 500 行),立刻调
sheet.FlushRows(500),已写行刷到磁盘临时文件 - 写完记得
workbook.WriteToFile("output.xlsx"),否则临时文件不会合并 - 如果用 EasyExcel(Java)或
github.com/xuri/excelize/v2,注意其SetRow默认非流式,需配合StreamWriter模式
导出必须走异步,别让 HTTP 连接卡住
Gin 是同步阻塞模型,一个 3 分钟的导出请求会占住一个 goroutine 和连接,QPS 直接归零。正确姿势是:
- 收到导出请求后,立即返回任务 ID(如
{"task_id": "exp_abc123"}) - 启动 goroutine 异步执行分页查 + 流式写入,结果存到临时目录(带过期清理)
- 提供
/api/export/status?task_id=xxx接口查进度,前端轮询 - 最终下载走独立路由(如
GET /api/export/download?task_id=xxx),用c.Header("Content-Disposition", ...)+c.File()返回文件
最容易被忽略的一点:临时 Excel 文件写完后,务必校验是否真能打开——有些流式库在最后一片 flush 前就 close 了 writer,导致文件头损坏,Excel 提示“文件已损坏”。建议加一步 os.Stat 检查文件大小 > 0,再用 excelize.OpenFile 尝试加载一次。



















