导出接口需正确设置Content-Type和Content-Disposition响应头,CSV须加UTF-8 BOM,Excel推荐excelize/v2;大文件应异步处理并配任务状态管理与安全文件存储。

导出接口必须设对 Content-Type 和 Content-Disposition
浏览器识别下载文件靠的就是这两个响应头,设错会导致文件打不开、乱码或直接在页面渲染成二进制流。常见错误是:Content-Type 写成 text/plain 或漏掉 charset=utf-8(CSV);Content-Disposition 用普通引号包裹中文名,导致 Safari/旧版 IE 解析失败。
正确做法:
- CSV:用
c.Header("Content-Type", "text/csv; charset=utf-8")+c.Header("Content-Disposition", "attachment; filename*=UTF-8''" + url.QueryEscape("报表.csv")) - Excel(.xlsx):用
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")+ 同样用filename*=UTF-8''格式 - 务必写 UTF-8 BOM(
[]byte{0xEF, 0xBB, 0xBF})给 CSV,否则 Excel 打开中文列名/内容会乱码(尤其 Windows 默认 Excel)
excelize/v2 是当前最稳的 Excel 导出选择
对比 github.com/tealeg/xlsx,excelize/v2 支持样式、公式、图表、多 sheet、流式写入,且纯 Go 实现、无 CGO 依赖,部署到 Alpine 容器也零问题。它也是官方文档和社区主流推荐方案。
注意几个关键点:
立即学习“go语言免费学习笔记(深入)”;
- 创建文件后记得
defer f.Close(),否则并发下可能 fd 泄露 - 写单元格用
f.SetCellValue("Sheet1", "A1", "姓名"),行列坐标必须是 Excel 格式字符串(如"B"+strconv.Itoa(i+2)),不能传整数坐标 - 设置活动 sheet 必须调用
f.SetActiveSheet(index),否则部分客户端(如 WPS)打开时可能默认显示空白 sheet - 大数据量别用
SetCellValue逐行写,改用f.SetSheetRow批量写入切片,性能提升明显
异步导出不是“加个 go 就完事”
用户点击导出按钮后,如果数据量大(比如查 50w 行 MySQL),同步写 Excel + 写响应体容易超时、阻塞 Gin 的 goroutine 调度。但简单起个 go func() { ... }() 有严重隐患:goroutine 中无法访问 *gin.Context,且没做任务状态跟踪、失败重试、清理机制。
可行方案要包含三块:
- API 接口只返回任务 ID(如
/export/start → {"task_id": "exp_abc123"}) - 后台用带缓冲 channel 或 worker pool 管理导出任务,写入临时文件到
ExportSavePath(如export/目录),并记录状态到内存 map 或 Redis - 另配一个
/export/status?task_id=xxx接口供前端轮询,以及/export/download?task_id=xxx提供文件流下载(注意校验权限和文件存在性)
导出路径和文件名生成要防冲突和越权
本地保存 Excel 文件时,若直接拼接用户输入的 filename 或 ID,可能引发路径遍历(如 ../../etc/passwd)或文件覆盖。更隐蔽的问题是并发写同名文件导致内容错乱。
安全做法:
- 存储路径固定为配置项
ExportSavePath(如export/),绝对不用用户可控路径 - 文件名用
uuid.NewString() + "_" + time.Now().Format("20060102150405") + ".xlsx",避免重复 - 下载接口中,从任务 ID 查出真实文件路径,再用
http.ServeFile或io.Copy流式输出,禁止直接拼接 URL 路径返回 - 临时文件需设置过期策略(如 24 小时自动清理),否则磁盘迟早被占满


















