大文件导出应采用流式响应:绕过gin默认缓冲,直接操作c.writer,手动设header与状态码,用csv.writer或excelize.writeto逐批写入并flush;前端需用readablestream+papaparse流式解析。

大文件导出时内存暴增,Gin默认响应缓冲区扛不住
直接用 c.JSON() 或 c.String() 返回几 MB 的表格数据(比如 10 万行 CSV/Excel JSON),Go 进程内存会瞬间涨几百 MB,甚至 OOM。Gin 默认把整个响应体写入内存 buffer 再 flush,没做流式处理。
真正要做的不是“发大数据”,而是“边生成边发”。关键在绕过 Gin 的中间件响应包装,直接操作 c.Writer,并禁用 Content-Length 自动计算(它会强制缓存全部内容)。
- 必须调用
c.Writer.WriteHeader(statusCode)手动写状态码,否则 Gin 会在最后自动写,导致 header 已发送后无法再改 - 设置
c.Header("Content-Disposition", "attachment; filename=data.csv")和c.Header("Content-Type", "text/csv; charset=utf-8") - 务必调用
c.Writer.Flush()在每批数据后主动刷出,避免被 Gin 的 responseWriter 缓存住 - 不要用
c.Data()或c.String()—— 它们内部仍走完整 buffer 流程
CSV 表格流式生成:用 csv.Writer 直接写到 c.Writer
这是最轻量、兼容性最好、客户端解析最稳的方式。不用第三方 Excel 库,不占额外内存,逐行写入即可。
// 示例:导出用户列表 CSV
func exportUsers(c *gin.Context) {
c.Header("Content-Type", "text/csv; charset=utf-8")
c.Header("Content-Disposition", "attachment; filename=users.csv")
writer := csv.NewWriter(c.Writer)
// 写表头
writer.Write([]string{"id", "name", "email", "created_at"})
c.Writer.Flush() // 确保表头立即发出
users, err := db.QueryUsers() // 假设返回 []User,支持游标或分页迭代
if err != nil {
http.Error(c.Writer, err.Error(), http.StatusInternalServerError)
return
}
for _, u := range users {
writer.Write([]string{
strconv.FormatInt(u.ID, 10),
u.Name,
u.Email,
u.CreatedAt.Format(time.RFC3339),
})
}
writer.Flush() // 刷完最后一行
}
注意:csv.Writer 默认带缓冲(2 KiB),对百万行也够用;如果字段含换行符或逗号,csv.Writer 会自动加引号转义,无需手动处理。
- 别在循环里反复调用
c.Writer.Flush()—— 每行 flush 一次反而降低吞吐,交给csv.Writer.Flush()统一控制更高效 - 若数据库查询本身不支持流式(如 MySQL 的
Rows是惰性读取),确保用rows.Next()+rows.Scan()逐行读,而非rows.Columns()全部加载
前端接收大 CSV 时卡死?不是后端问题,是浏览器没做流式解析
后端已流式输出,但前端用 fetch().then(r => r.text()) 仍会等全部下载完才解析,内存照样爆。必须用 ReadableStream 边收边处理。
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
- 用
response.body.getReader()获取流读取器,配合TextDecoder解码 UTF-8 - 按行切割不能简单用
split('\n')—— CSV 行内可能有换行符,应交由前端 csv-parse 库(如papaparse的parseChunk模式)流式解析 - 若只是下载不解析,直接用
URL.createObjectURL(new Blob([response.body]))创建临时链接,浏览器自行处理流式写磁盘
示例关键片段:
const reader = response.body.getReader();
const decoder = new TextDecoder('utf-8');
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
Papa.parse(chunk, {
worker: false,
chunk: processData, // 每块 CSV 数据回调
});
}
想返回 Excel(.xlsx)?别用 excelize 直接写响应体
excelize 的 f.Write() 会把整个 xlsx 二进制写入内存 buffer,和 JSON 一样崩。正确做法是用 f.WriteTo(c.Writer) —— 它底层调用 io.Copy,支持流式写入。
- 必须提前设置好所有 sheet、样式、公式,再调用
WriteTo;不能边写边发(xlsx 是 ZIP 结构,header 依赖 footer 信息) - 即便流式写入,Excel 文件本身仍需完整生成在内存中(因为 ZIP 必须知道所有文件大小才能封包),所以超过 50MB 就该切分成多个文件或改用 CSV
- 若必须单文件大 Excel,建议用
excelize的f.SaveAs("/tmp/file.xlsx")写磁盘,再用c.File("/tmp/file.xlsx")—— Gin 的File内部用io.Copy,不会全读进内存
流式写入示例:
f := excelize.NewFile()
f.SetSheetRow("Sheet1", "A1", &[]interface{}{"ID", "Name", "Email"})
// ... 写入大量数据
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Header("Content-Disposition", "attachment; filename=data.xlsx")
f.WriteTo(c.Writer) // 关键:不是 f.Write()
真正卡点不在框架,而在数据生成方式是否与 HTTP 流匹配。CSV 流式最稳,Excel 要么小文件流写,要么落地后 File 发送。别让中间件、编码库、前端解析三处同时缓存同一份数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










