Go Excelize 怎么流式写入大量工作表行
来源:17golang原创
时间:2026-10-13 17:38:16 117浏览 收藏
我在做 XLSX 导出时,最先遇到的不是 Excelize API 难,而是“把所有行先装进一个大切片”这件事会让内存随着数据量一起涨。Go Excelize 针对这种场景提供了 StreamWriter:先拿到流式写入器,再按递增行号写入每一行,最后调用 Flush,接着保存文件。
这篇只解决一个问题:Go Excelize 怎么流式写入大量工作表行。示例使用官方当前仓库与流式写入文档中的 API 语义,不把流式写入包装成 Excelize 的全部功能介绍。
官方地址:https://github.com/qax-os/excelize
我为什么从一次性写入换成 StreamWriter
一次性写入的代码看起来很直观:先构造包含全部记录的二维切片,再交给工作表 API。数据只有几千行时,这种方式没有明显压力;但当导出任务达到几十万行,数据对象、切片容量和 XLSX 组装过程会同时占用内存,导出任务就容易和业务请求争抢资源。
Excelize 的流式方案把关注点收窄到“当前要写的行”。官方文档说明,NewStreamWriter 用于向已经存在的空白工作表写入大量数据;当内存中的数据块超过 16MB 时,流式写入器会尝试使用临时文件来降低内存占用。这里的关键不是保证内存永远固定,而是避免把所有待写行长期保留在应用自己的大切片里。
我的判断是:导出数据量稳定且很小,普通 API 更省心;导出行数明显增长、又不需要在同一张表里边写边读时,StreamWriter 才值得引入。

用 NewStreamWriter 按递增行号写入行
最小实现可以分成四个对象关系:创建工作簿,获取现有空白工作表的流式写入器,按行号调用 SetRow,最后结束流。行坐标可以用 CoordinatesToCellName 从列号和行号生成,避免手工拼接大量坐标字符串。
package main
import (
"fmt"
"log"
"github.com/xuri/excelize/v2"
)
func main() {
f := excelize.NewFile()
// 文件对象最终要关闭,避免导出任务留下未释放的资源。
defer func() {
if err := f.Close(); err != nil {
log.Printf("关闭工作簿失败: %v", err)
}
}()
// Sheet1 是新工作簿自带的空白工作表,适合交给流式写入器。
sw, err := f.NewStreamWriter("Sheet1")
if err != nil {
log.Fatalf("创建流式写入器失败: %v", err)
}
// 列宽属于写行前的结构设置,先设置可以避免和后续行写入混用。
if err := sw.SetColWidth(1, 3, 18); err != nil {
log.Fatalf("设置列宽失败: %v", err)
}
headers := []interface{}{"编号", "名称", "状态"}
if err := sw.SetRow("A1", headers); err != nil {
log.Fatalf("写入表头失败: %v", err)
}
for rowID := 2; rowID
这段代码里最容易忽略的是两个顺序:SetRow 的行号从 1 往后递增,Flush 在 SaveAs 之前。写入单元格的值可以是普通值,也可以使用 Excelize 的 Cell 指定样式或公式;但不要因此把整张表重新组织成大对象后再写,流式收益仍然来自逐行准备数据。
Flush、SaveAs 和 Close 是三个不同的收尾动作
第一次接触流式 API 时,我很容易把“写完最后一行”当成“文件已经完成”。实际上,SetRow 只是把行交给流式写入器,Flush 才是结束流式写入过程;之后还要用 SaveAs 把工作簿保存成目标文件,最后由 Close 释放文件对象。
可以把它们分别理解成:
SetRow:持续追加一行,要求行号递增。Flush:结束当前工作表的流式写入。SaveAs:把已经完成的工作簿写到目标路径。Close:关闭工作簿对象,处理底层资源。
如果中途任何一个写入动作返回错误,就应该停止继续追加,并保留错误上下文。特别是不要在 Flush 失败后继续调用保存动作,把一个未完成的流误当成成功文件。
流式写入的几个限制,决定了它适不适合你

工作表要先处于适合流式写入的状态
NewStreamWriter 面向已经存在的空白工作表。新建工作簿自带 Sheet1,直接使用它最简单;如果要使用其他工作表,先创建或准备好工作表,再获取对应的流式写入器。不要把一个已经用普通模式写过大量内容的工作表直接当作空白流来处理。
不能把普通写入 API 和流式写入混在同一张表
官方文档明确提示,普通模式函数和流式模式函数不能混合用于同一张工作表的写入。这个限制意味着:需要设置的列宽、窗格或其他流式支持的结构选项,应在写行前处理;进入 SetRow 之后,就保持同一种写入模式,避免一半数据走 SetCellValue、另一半走 SetRow。
流式阶段不要依赖读回单元格
流式写入器会尽量把大块数据放到临时文件以减少内存压力,但官方 API 说明也指出,流式阶段不能读取单元格值。因此,校验业务数据应在调用 SetRow 前完成,或者在文件完成后另开读取流程;不要边写边用当前工作簿读取刚写入的值。
行号递增是接口约束,不是建议
如果数据来自分页查询或多个来源,先在应用层确定最终行号,再按从小到大的顺序调用 SetRow。不要为了插入一条迟到的数据回头写前面的行;需要重排时,先调整数据源输出顺序,再进入流式写入阶段。
我会怎样选择普通写入还是流式写入
| 场景 | 更合适的选择 | 判断理由 |
|---|---|---|
| 少量行、需要频繁修改单元格 | 普通写入 API | 代码更直观,修改和读取更灵活。 |
| 大量行、主要目标是顺序导出 | StreamWriter | 逐行准备数据,减少应用侧长期保留全部行。 |
| 同一张表既要回写又要边写边读 | 重新设计阶段边界 | 流式模式不适合和普通写入、读回动作混在一起。 |
| 需要表头、列宽和数据行 | 先做支持的结构设置,再 SetRow | 避免在流已经开始后改变写入模式。 |
对我来说,StreamWriter 的价值不是让任何 Excel 导出都更快,而是让“顺序生成大量行”这件事有清晰的资源边界。它的代价也很明确:要遵守递增行号、不能混用同表写入 API、不能依赖流式阶段的读取,还必须记得 Flush。
常见问题
Excelize 流式写入为什么一定要调用 Flush?
Flush 用来结束流式写入过程。只调用最后一次 SetRow 并不等于流已经收尾,应该在 SaveAs 之前检查 Flush 的错误。
SetRow 能不能乱序写入行号?
不建议,也不符合流式写入约束。应该让传给 SetRow 的行号保持递增;如果数据乱序,先在数据准备阶段排序或重新编号。
流式写入是否完全不占用内存?
不是。它减少的是应用长期保存全部待写行的压力,仍然会有当前行、工作簿对象和流式缓冲;Excelize 还可能在数据块较大时使用临时文件。应把它理解为更可控的写入方式,而不是零内存方案。
总结
Go Excelize 流式写入大量工作表行的核心写法是:对空白工作表调用 NewStreamWriter,按递增行号调用 SetRow,写完后调用 Flush,再 SaveAs 并关闭工作簿。只要把普通写入与流式写入的边界、读取限制和资源收尾记清楚,就能把大表导出从“把所有数据堆在内存里”改成更适合顺序生成的工作流。
-
860 收藏
-
843 收藏
-
826 收藏
-
809 收藏
-
792 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习