Golang

关注公众号 jb51net

关闭
首页 > 脚本专栏 > Golang > Go百万级数据导出

纯Go语言实现百万级数据高效导出Excel和CSV实战指南

作者:枫叶V

这篇文章主要为大家详细介绍了如何通过纯Go语言实现百万级数据高效导出Excel和CSV的相关知识,文中的示例代码讲解详细,需要的小伙伴可以了解下

只用 Go,一篇文章吃透“大数据导出”的性能坑流式思路、与可直接跑的完整示例

重点:极低内存可中断可并发可在 HTTP 中直接下载

为什么大数据导出总“炸内存”

核心原则分页查询 + 流式写入 + 限流并发 + 可中断(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 深翻导致的回表代价。

取消/限流/超时与资源回收

ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute) // HTTP 请求 5 分钟超时
defer cancel()

// 在所有 QueryContext/SetRow 处传递 ctx,遇到取消/超时立即停止

样式/列宽/冻结窗格

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百万级数据导出内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文