純Go語(yǔ)言實(shí)現(xiàn)百萬(wàn)級(jí)數(shù)據(jù)高效導(dǎo)出Excel和CSV實(shí)戰(zhàn)指南
只用 Go,一篇文章吃透“大數(shù)據(jù)導(dǎo)出”的性能坑、流式思路、與可直接跑的完整示例。
重點(diǎn):極低內(nèi)存、可中斷、可并發(fā)、可在 HTTP 中直接下載。
為什么大數(shù)據(jù)導(dǎo)出總“炸內(nèi)存”
- 一次性加載:先查全量數(shù)據(jù)再寫(xiě)文件 → O(N) 內(nèi)存直接拉滿。
- 非流式寫(xiě)入:許多庫(kù)默認(rèn)把整表緩存在內(nèi)存里,再一次性落盤(pán)。
- 文件格式差異:XLSX 是壓縮的 XML 打包;CSV 則是純文本。CSV 寫(xiě)入更快內(nèi)存更省,但不支持樣式/合并等。
核心原則:分頁(yè)查詢 + 流式寫(xiě)入 + 限流并發(fā) + 可中斷(Context)。
技術(shù)選型(純 Go)
| 格式 | 庫(kù)/包 | 優(yōu)點(diǎn) | 注意點(diǎn) |
|---|---|---|---|
| XLSX | [github.com/xuri/excelize/v2] | 支持樣式/多表/列寬/凍結(jié)窗格;有StreamWriter | 寫(xiě)樣式需要謹(jǐn)慎,流式寫(xiě)下不建議頻繁變更樣式 |
| CSV | 標(biāo)準(zhǔn)庫(kù) encoding/csv | 依賴零、最快、最省內(nèi)存 | 無(wú)樣式、用 Excel 打開(kāi)時(shí)默認(rèn)編碼/分隔符需注意 |
最小可跑:流式寫(xiě)出 100 萬(wàn)行 XLSX
關(guān)鍵:StreamWriter + SetRow + Flush,最后 f.Write(io.Writer)/f.SaveAs(path)。
內(nèi)存監(jiān)控: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)
// 寫(xiě)表頭
header := []interface{}{"ID", "Name", "Amount", "CreatedAt"}
cell, _ := excelize.CoordinatesToCellName(1, 1)
must(sw.SetRow(cell, header))
// 模擬寫(xiě) 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 行打印一次內(nèi)存占用
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())
// 可選:凍結(jié)首行 + 自動(dòng)列寬(注意:自動(dòng)列寬對(duì)流式無(wú)感,需在 Flush 后做固定寬度)
// 凍結(jié)首行
must(f.SetPanes(sheet, &excelize.Panes{
Freeze: true,
YSplit: 1,
ActivePane: "bottomLeft",
TopLeftCell: "A2",
}))
// 保存到磁盤(pán)(也可改為寫(xiě)到 HTTP ResponseWriter,見(jiàn)后文)
must(f.SaveAs("bigdata.xlsx"))
fmt.Printf("done in %v\n", time.Since(start))
}
func must(err error) {
if err != nil {
panic(err)
}
}
效果:在普通開(kāi)發(fā)機(jī)上可穩(wěn)定導(dǎo)出百萬(wàn)行,堆內(nèi)存通常幾十到百余 MB(視系統(tǒng)/版本而定),遠(yuǎn)低于一次性構(gòu)建整表的做法。
極致輕量:CSV 流式導(dǎo)出
零依賴、寫(xiě)入速度快、最省內(nèi)存。
Excel 可以直接打開(kāi)(注意編碼/分隔符)。
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() // 顯式?jīng)_刷,降低緩沖占用
}
}
}HTTP 直接下載(無(wú)中間文件)
邊寫(xiě)邊下:避免臨時(shí)大文件;但需要瀏覽器保持連接,失敗要能重試。
XLSX:流式寫(xiě)到 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 或業(yè)務(wù)層拿
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()
// 直接寫(xiě)到 ResponseWriter
if err := f.Write(w); err != nil {
// 客戶端中斷下載常見(jiàn):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() }數(shù)據(jù)庫(kù)分頁(yè):生產(chǎn)-消費(fèi)流式管道
目標(biāo):不在內(nèi)存里堆積全量數(shù)據(jù)。
方案:分頁(yè)拉取(或游標(biāo)查詢)→ 放入 channel → 單一 writer 順序?qū)懗觥?/p>
package main
import (
"context"
"database/sql"
"fmt"
"time"
_ "modernc.org/sqlite" // 為了示例,用 SQLite;生產(chǎn)用 MySQL/PostgreSQL 驅(qū)動(dòng)
)
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/游標(biāo)翻頁(yè))
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: // 下游慢時(shí)自然限速
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) // 小緩沖,避免放大內(nèi)存
go func() {
_ = StreamRows(ctx, db, 5000, out)
}()
// 在這里對(duì)接 XLSX/CSV Writer(單寫(xiě)者,保持順序)
for r := range out {
// sw.SetRow(...)/csv.Write(...)
_ = r
}
return ctx.Err()
}
func main() {
fmt.Println("見(jiàn) ExamplePipeline;將其與前文 XLSX/CSV 寫(xiě)入代碼拼裝即可。")
}
Keyset 翻頁(yè)(基于上次最大 ID)在 MySQL 大表場(chǎng)景下更穩(wěn)更快,避免 OFFSET 深翻導(dǎo)致的回表代價(jià)。
取消/限流/超時(shí)與資源回收
- Context:貫穿 DB 查詢、業(yè)務(wù)、Writer,允許用戶在前端取消下載時(shí)快速停水關(guān)閘。
- 限流:time.Ticker 控制每秒寫(xiě)入/查詢頁(yè)數(shù),避免壓垮 DB。
- 回收:defer rows.Close()/file.Close()/sw.Flush(),對(duì)網(wǎng)絡(luò)中斷容錯(cuò)。
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Minute) // HTTP 請(qǐng)求 5 分鐘超時(shí) defer cancel() // 在所有 QueryContext/SetRow 處傳遞 ctx,遇到取消/超時(shí)立即停止
樣式/列寬/凍結(jié)窗格
- 凍結(jié)首行:見(jiàn)前文 SetPanes 示例。
- 列寬:SetColWidth(sheet, "A", "D", 18),注意:StreamWriter 模式下不要頻繁改動(dòng),否則性能受損。
- 樣式:可用 NewStyle + SetCellStyle,但大數(shù)據(jù)導(dǎo)出建議樣式極簡(jiǎn)(僅表頭設(shè)一下)。
styleID, _ := f.NewStyle(&excelize.Style{Font: &excelize.Font{Bold: true}})
_ = f.SetCellStyle("Sheet1", "A1", "D1", styleID)
_ = f.SetColWidth("Sheet1", "A", "D", 18)
常見(jiàn)坑與對(duì)策清單
1. 導(dǎo)出極慢
關(guān)掉逐格設(shè)置樣式/公式;使用 SetRow 成批寫(xiě)
2. 內(nèi)存持續(xù)上漲
確認(rèn)使用 StreamWriter;減少中間切片累積;分頁(yè)尺寸控制在 1k~10k。
3.瀏覽器中斷下載
日志常見(jiàn) broken pipe,屬正常情況;上游應(yīng)放棄繼續(xù)寫(xiě)入并回收資源。
4.Excel 打開(kāi) CSV 亂碼
使用 ; charset=utf-8;Windows 場(chǎng)景可考慮 BOM(\xEF\xBB\xBF)或?qū)С?XLSX。
5.OFFSET 翻頁(yè)抖動(dòng)
大表使用 keyset(WHERE id > last_id ORDER BY id LIMIT ?)。
6.導(dǎo)出耗時(shí)過(guò)長(zhǎng)
優(yōu)化 SQL(只查必要列、加索引)、縮小時(shí)間/條件范圍、服務(wù)端先異步生成后提供下載(任務(wù)輪詢)。
7.并發(fā)順序問(wèn)題
多協(xié)程查詢 + 單 Writer 順序?qū)?;或?Sheet 并行,每個(gè) Sheet 一個(gè) Writer。
總結(jié)
首選 CSV:海量數(shù)據(jù)、追求極致性能與穩(wěn)定,CSV 基本無(wú)敵。
需要樣式就上 XLSX:務(wù)必使用 StreamWriter,樣式從簡(jiǎn),分頁(yè)拉取。
端到端流式:DB → channel → 單寫(xiě)者 → 文件/HTTP,配合 Context 實(shí)現(xiàn)可中斷與資源即時(shí)回收。
到此這篇關(guān)于純Go語(yǔ)言實(shí)現(xiàn)百萬(wàn)級(jí)數(shù)據(jù)高效導(dǎo)出Excel和CSV實(shí)戰(zhàn)指南的文章就介紹到這了,更多相關(guān)Go百萬(wàn)級(jí)數(shù)據(jù)導(dǎo)出內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang獲取prometheus數(shù)據(jù)(prometheus/client_golang包)
本文主要介紹了使用Go語(yǔ)言的prometheus/client_golang包來(lái)獲取Prometheus監(jiān)控?cái)?shù)據(jù),具有一定的參考價(jià)值,感興趣的可以了解一下2025-03-03
Go語(yǔ)言結(jié)合GORM實(shí)現(xiàn)數(shù)據(jù)庫(kù)事務(wù)處理的示例代碼
這篇文章主要為大家詳細(xì)介紹了Go語(yǔ)言結(jié)合GORM實(shí)現(xiàn)數(shù)據(jù)庫(kù)事務(wù)處理的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2025-08-08
Golang對(duì)struct字段重新排序優(yōu)化數(shù)據(jù)結(jié)構(gòu)性能實(shí)踐
這篇文章主要為大家介紹了Golang對(duì)struct字段重新排序優(yōu)化數(shù)據(jù)結(jié)構(gòu)性能實(shí)踐,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12
Golang中json和jsoniter的區(qū)別使用示例
這篇文章主要介紹了Golang中json和jsoniter的區(qū)別使用示例,本文給大家分享兩種區(qū)別,結(jié)合示例代碼給大家介紹的非常詳細(xì),感興趣的朋友跟隨小編一起看看吧2023-12-12

