纯Go语言实现百万级数据高效导出Excel和CSV实战指南
作者:枫叶V
只用 Go,一篇文章吃透“大数据导出”的性能坑、流式思路、与可直接跑的完整示例。
重点:极低内存、可中断、可并发、可在 HTTP 中直接下载。
为什么大数据导出总“炸内存”
- 一次性加载:先查全量数据再写文件 → O(N) 内存直接拉满。
- 非流式写入:许多库默认把整表缓存在内存里,再一次性落盘。
- 文件格式差异:XLSX 是压缩的 XML 打包;CSV 则是纯文本。CSV 写入更快内存更省,但不支持样式/合并等。
核心原则:分页查询 + 流式写入 + 限流并发 + 可中断(Context)。
技术选型(纯 Go)
| 格式 | 库/包 | 优点 | 注意点 |
|---|---|---|---|
| XLSX | [github.com/xuri/excelize/v2] | 支持样式/多表/列宽/冻结窗格;有StreamWriter | 写样式需要谨慎,流式写下不建议频繁变更样式 |
| CSV | 标准库 encoding/csv | 依赖零、最快、最省内存 | 无样式、用 Excel 打开时默认编码/分隔符需注意 |
最小可跑:流式写出 100 万行 XLSX
关键:StreamWriter + SetRow + Flush,最后 f.Write(io.Writer)/f.SaveAs(path)。
内存监控:runtime.ReadMemStats。
// go mod init example && go get github.com/xuri/excelize/v2
package main
import (
"fmt"
"math/rand"
"runtime"
"strconv"
"time"
"github.com/xuri/excelize/v2"
)
func main() {
start := time.Now()
f := excelize.NewFile()
defer f.Close()
const sheet = "Sheet1"
sw, err := f.NewStreamWriter(sheet)
must(err)
// 写表头
header := []interface{}{"ID", "Name", "Amount", "CreatedAt"}
cell, _ := excelize.CoordinatesToCellName(1, 1)
must(sw.SetRow(cell, header))
// 模拟写 1,000,000 行
n := 1_000_000
for i := 1; i <= n; i++ {
row := []interface{}{
i,
"User_" + strconv.Itoa(i),
rand.Intn(10_000),
time.Now().Add(time.Duration(i) * time.Second).Format(time.RFC3339),
}
cell, _ := excelize.CoordinatesToCellName(1, i+1)
must(sw.SetRow(cell, row))
// 每 50k 行打印一次内存占用
if i%50_000 == 0 {
var m runtime.MemStats
runtime.ReadMemStats(&m)
fmt.Printf("[progress] rows=%d heap=%.2fMB\n", i, float64(m.HeapAlloc)/1024.0/1024.0)
}
}
must(sw.Flush())
// 可选:冻结首行 + 自动列宽(注意:自动列宽对流式无感,需在 Flush 后做固定宽度)
// 冻结首行
must(f.SetPanes(sheet, &excelize.Panes{
Freeze: true,
YSplit: 1,
ActivePane: "bottomLeft",
TopLeftCell: "A2",
}))
// 保存到磁盘(也可改为写到 HTTP ResponseWriter,见后文)
must(f.SaveAs("bigdata.xlsx"))
fmt.Printf("done in %v\n", time.Since(start))
}
func must(err error) {
if err != nil {
panic(err)
}
}
效果:在普通开发机上可稳定导出百万行,堆内存通常几十到百余 MB(视系统/版本而定),远低于一次性构建整表的做法。
极致轻量:CSV 流式导出
零依赖、写入速度快、最省内存。
Excel 可以直接打开(注意编码/分隔符)。
package main
import (
"encoding/csv"
"fmt"
"os"
"strconv"
)
func main() {
file, err := os.Create("bigdata.csv")
if err != nil {
panic(err)
}
defer file.Close()
w := csv.NewWriter(file)
defer w.Flush()
// 表头
_ = w.Write([]string{"ID", "Name", "Age"})
const n = 1_000_000
for i := 1; i <= n; i++ {
_ = w.Write([]string{
strconv.Itoa(i),
"User_" + strconv.Itoa(i),
strconv.Itoa(18 + i%30),
})
if i%100_000 == 0 {
fmt.Println("rows:", i)
w.Flush() // 显式冲刷,降低缓冲占用
}
}
}HTTP 直接下载(无中间文件)
边写边下:避免临时大文件;但需要浏览器保持连接,失败要能重试。
XLSX:流式写到 ResponseWriter(示例代码)
package main
import (
"log"
"net/http"
"strconv"
"time"
"github.com/xuri/excelize/v2"
)
func main() {
http.HandleFunc("/export/xlsx", exportXLSX)
http.HandleFunc("/export/csv", exportCSV)
log.Println("listen :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func exportXLSX(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
w.Header().Set("Content-Disposition", "attachment; filename=\"report.xlsx\"")
f := excelize.NewFile()
defer f.Close()
sw, err := f.NewStreamWriter("Sheet1")
if err != nil {
http.Error(w, err.Error(), 500)
return
}
_ = sw.SetRow("A1", []interface{}{"ID", "Name", "Amount", "CreatedAt"})
n := 200_000 // 从 query 或业务层拿
for i := 1; i <= n; i++ {
cell, _ := excelize.CoordinatesToCellName(1, i+1)
_ = sw.SetRow(cell, []interface{}{
i,
"User_" + strconv.Itoa(i),
i % 1000,
time.Now().Format(time.RFC3339),
})
}
_ = sw.Flush()
// 直接写到 ResponseWriter
if err := f.Write(w); err != nil {
// 客户端中断下载常见:write: broken pipe
log.Println("write err:", err)
}
}
func exportCSV(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/csv; charset=utf-8")
w.Header().Set("Content-Disposition", "attachment; filename=\"report.csv\"")
cw := NewCSVStream(w)
defer cw.Flush()
_ = cw.Write([]string{"ID", "Name", "Age"})
for i := 1; i <= 500_000; i++ {
_ = cw.Write([]string{
strconv.Itoa(i),
"User_" + strconv.Itoa(i),
strconv.Itoa(18 + i%30),
})
}
}
// 轻薄包装:将 http.ResponseWriter 适配成 csv.Writer
type CSVStream struct{ w *csv.Writer }
func NewCSVStream(w http.ResponseWriter) *CSVStream { return &CSVStream{w: csv.NewWriter(w)} }
func (c *CSVStream) Write(rec []string) error { return c.w.Write(rec) }
func (c *CSVStream) Flush() { c.w.Flush() }数据库分页:生产-消费流式管道
目标:不在内存里堆积全量数据。
方案:分页拉取(或游标查询)→ 放入 channel → 单一 writer 顺序写出。
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "modernc.org/sqlite" // 为了示例,用 SQLite;生产用 MySQL/PostgreSQL 驱动
)
type Row struct {
ID int64
Name string
Amount int64
CreatedAt time.Time
}
func StreamRows(ctx context.Context, db *sql.DB, pageSize int, out chan<- Row) error {
defer close(out)
offset := 0
for {
// 替换为你的 SQL(MySQL 推荐 keyset/游标翻页)
rows, err := db.QueryContext(ctx,
`SELECT id, name, amount, created_at FROM t ORDER BY id LIMIT ? OFFSET ?`,
pageSize, offset,
)
if err != nil {
return err
}
n := 0
for rows.Next() {
var r Row
if err := rows.Scan(&r.ID, &r.Name, &r.Amount, &r.CreatedAt); err != nil {
_ = rows.Close()
return err
}
select {
case out <- r: // 下游慢时自然限速
case <-ctx.Done():
_ = rows.Close()
return ctx.Err()
}
n++
}
_ = rows.Close()
if n == 0 {
return nil // 完成
}
offset += n
}
}
func ExamplePipeline(ctx context.Context, db *sql.DB) error {
out := make(chan Row, 1024) // 小缓冲,避免放大内存
go func() {
_ = StreamRows(ctx, db, 5000, out)
}()
// 在这里对接 XLSX/CSV Writer(单写者,保持顺序)
for r := range out {
// sw.SetRow(...)/csv.Write(...)
_ = r
}
return ctx.Err()
}
func main() {
fmt.Println("见 ExamplePipeline;将其与前文 XLSX/CSV 写入代码拼装即可。")
}
Keyset 翻页(基于上次最大 ID)在 MySQL 大表场景下更稳更快,避免 OFFSET 深翻导致的回表代价。
取消/限流/超时与资源回收
- Context:贯穿 DB 查询、业务、Writer,允许用户在前端取消下载时快速停水关闸。
- 限流:time.Ticker 控制每秒写入/查询页数,避免压垮 DB。
- 回收:defer rows.Close()/file.Close()/sw.Flush(),对网络中断容错。
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute) // HTTP 请求 5 分钟超时 defer cancel() // 在所有 QueryContext/SetRow 处传递 ctx,遇到取消/超时立即停止
样式/列宽/冻结窗格
- 冻结首行:见前文 SetPanes 示例。
- 列宽:SetColWidth(sheet, "A", "D", 18),注意:StreamWriter 模式下不要频繁改动,否则性能受损。
- 样式:可用 NewStyle + SetCellStyle,但大数据导出建议样式极简(仅表头设一下)。
styleID, _ := f.NewStyle(&excelize.Style{Font: &excelize.Font{Bold: true}})
_ = f.SetCellStyle("Sheet1", "A1", "D1", styleID)
_ = f.SetColWidth("Sheet1", "A", "D", 18)
常见坑与对策清单
1. 导出极慢
关掉逐格设置样式/公式;使用 SetRow 成批写
2. 内存持续上涨
确认使用 StreamWriter;减少中间切片累积;分页尺寸控制在 1k~10k。
3.浏览器中断下载
日志常见 broken pipe,属正常情况;上游应放弃继续写入并回收资源。
4.Excel 打开 CSV 乱码
使用 ; charset=utf-8;Windows 场景可考虑 BOM(\xEF\xBB\xBF)或导出 XLSX。
5.OFFSET 翻页抖动
大表使用 keyset(WHERE id > last_id ORDER BY id LIMIT ?)。
6.导出耗时过长
优化 SQL(只查必要列、加索引)、缩小时间/条件范围、服务端先异步生成后提供下载(任务轮询)。
7.并发顺序问题
多协程查询 + 单 Writer 顺序写;或多 Sheet 并行,每个 Sheet 一个 Writer。
总结
首选 CSV:海量数据、追求极致性能与稳定,CSV 基本无敌。
需要样式就上 XLSX:务必使用 StreamWriter,样式从简,分页拉取。
端到端流式:DB → channel → 单写者 → 文件/HTTP,配合 Context 实现可中断与资源即时回收。
到此这篇关于纯Go语言实现百万级数据高效导出Excel和CSV实战指南的文章就介绍到这了,更多相关Go百万级数据导出内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
