因为excel是二进制格式,直接用c.string()或c.json()会错误设置content-type为text/plain或application/json,导致浏览器无法识别,出现空文件、乱码或html渲染;正确做法是手动设置content-type和content-disposition响应头,并用c.data()或c.datafromreader()写入原始字节流。

为什么直接用 gin.Context.Writer 写 Excel 文件会出错
因为 Excel 文件(尤其是 .xlsx)是二进制格式,不是纯文本。如果直接用 c.String() 或 c.JSON() 强行输出,Gin 会自动设置 Content-Type: text/plain 或 application/json,浏览器无法识别,可能下载为空文件、乱码,或被当成 HTML 渲染。
正确做法是手动设置响应头,并写入原始字节流:
- 调用
c.Writer.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet") - 设置
Content-Disposition告诉浏览器这是附件且指定文件名,例如:attachment; filename="report.xlsx" - 用
c.Writer.Write(data)(data是[]byte)或io.Copy(c.Writer, file)输出
用 github.com/xuri/excelize/v2 生成 Excel 并响应给前端
这是目前 Go 生态最稳定、支持 .xlsx 读写的库,兼容 Office 和 WPS,无需依赖外部程序。注意不要用已归档的 tealeg/xlsx,它不支持新格式且长期未维护。
典型流程是:创建工作簿 → 写入数据 → 保存到内存(*bytes.Buffer)→ 写入响应体:
func exportExcel(c *gin.Context) {
f := excelize.NewFile()
index := f.NewSheet("Sheet1")
f.SetCellValue("Sheet1", "A1", "用户名")
f.SetCellValue("Sheet1", "B1", "订单数")
f.SetCellValue("Sheet1", "A2", "张三")
f.SetCellValue("Sheet1", "B2", 42)
f.DeleteSheet("Sheet1") // 默认 Sheet1 是空的,删掉再用新 index
f.SetActiveSheet(index)
buf := &bytes.Buffer{}
if err := f.Write(buf); err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Header("Content-Disposition", `attachment; filename="user_report.xlsx"`)
c.Data(200, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", buf.Bytes())
}
关键点:
-
f.Write(buf)是核心,它把整个工作簿序列化为.xlsx字节流 -
c.Data()比c.Writer.Write()更安全,它自动处理状态码和 header 发送顺序 - 不要漏掉
f.DeleteSheet("Sheet1")——NewFile()默认带一个空 sheet,若不删,导出后会有两个 sheet(一个空一个有数据)
大数据量导出时内存暴涨?用 io.Pipe 流式写入
当要导出上万行数据时,全量构建 excelize.File 再 Write() 到 bytes.Buffer,会导致内存峰值翻倍(一份在内存中的结构体 + 一份序列化后的字节)。这时应避免中间缓冲,让 Excel 写入和 HTTP 响应同步进行。
方案是用 io.Pipe 创建管道,一边 goroutine 向 pipe 写 Excel,一边由 Gin 直接从 pipe 读取并返回:
func exportLargeExcel(c *gin.Context) {
pr, pw := io.Pipe()
defer pr.Close()
go func() {
defer pw.Close()
f := excelize.NewFile()
index := f.NewSheet("Data")
f.SetCellValue("Data", "A1", "ID")
f.SetCellValue("Data", "B1", "Name")
for i := 1; i
<p>注意:</p>
-
c.DataFromReader()是 Gin 提供的流式响应方法,第二个参数传-1表示长度未知(适合 pipe) - 必须用
defer pw.Close(),否则 pipe reader 会永远阻塞 - 该方式内存占用基本恒定(约几 MB),但要注意并发导出时 goroutine 开销
中文乱码、样式丢失、公式不计算?检查这三点
Excel 导出后打开显示方块、无边框、SUM 公式显示为 0,通常不是 Gin 的问题,而是 excelize 使用细节导致:
- 中文正常显示的前提是:单元格字体显式设为支持中文的字体,例如
f.SetCellStyle("Sheet1", "A1", "B1", styleID)配合f.NewStyle(&excelize.Style{Font: &excelize.Font{Family: "微软雅黑"}});默认字体在某些系统下不生效 - 样式需先调用
f.NewStyle()获取 style ID,再用SetCellStyle()应用,不能只设 Font 就完事 - 公式(如
=SUM(A2:A100))必须用f.SetCellFormula()设置,且导出前调用f.Calculate()才能预计算结果(否则 Excel 打开时才计算,部分环境可能不触发)
这些细节不报错,但效果肉眼可见——导出的文件看似“成功”,实际交付给业务方时会被打回来重做。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











