应避免用 gorm.find() 导出大批量数据,因其全量加载导致内存溢出;正确做法是用 *sql.rows 流式 scan 并立即写入,复用预编译语句与 interface{} 切片以控内存。

别用 GORM 直接 Find + json.Marshal 或 csv.WriteAll 导出大批量数据——内存会爆、连接会卡、CSV 会空、Excel 会乱码,核心矛盾是“全量加载”和“流式生成”的错配。
为什么 GORM.Find() 导出百万行必崩
Find() 默认把所有记录一次性加载进内存,哪怕你只取 ID 和 Name 两个字段,GORM 仍会为每行分配 struct、填充字段、维护关联关系(即使没用)。导出 100 万行中等结构体,轻松吃掉 1–2GB 内存;MySQL 驱动还可能缓存整批结果(尤其启用了 parseTime=true),进一步放大 OOM 风险。
- rows.Next() 后必须立刻调
rows.Scan(),不 Scan 就等于没消费,底层驱动持续缓存 - 别用
rows.Slice()或rows.Map()封装——它们只是语法糖,内部仍要 Scan,且额外分配 slice/map - 导出前务必
db.Session(&gorm.Session{PrepareStmt: true})复用预编译语句,避免 SQL 解析瓶颈
正确姿势:用 sql.Rows + 流式写入
绕过 GORM 的 model 层,直连 *sql.Rows,每行 Scan 后立即写入目标格式。这是唯一能控制内存上限的方式。
- 复用
values []interface{}和valuePtrs []interface{}切片,避免每行 new 分配 - CSV 场景:用
bufio.NewWriterSize(f, 1 包一层 <code>csv.Writer,写完必须w.Flush()和buf.Flush() - Web 响应导出:w 是
http.ResponseWriter,循环写完后显式调w.(http.Flusher).Flush(),防超时断连 - Excel 场景:用
excelize.NewStreamWriter("Sheet1"),禁止f.NewSheet()后全量写入;中文需提前设字体样式,如Font.Name = "Noto Sans CJK SC"
时间字段、NULL 和特殊字符怎么处理
数据库字段类型和 Go 类型不完全对齐,直接 Scan 容易 panic 或丢数据。
- 含 NULL 的列(如
updated_at *time.Time)必须用sql.NullTime接收,再判.Valid转换 - 时间字段写 CSV 前统一转
t.Format("2006-01-02 15:04:05"),避免时区或零值问题 - CSV 字段含逗号、换行、引号?必须用
w.Write([]string{...}),别用fmt.Fprintf拼字符串——它不会自动加引号和转义 - JSON 导出禁用
omitempty,比如Age int `json:"age"`,否则Age: 0会被跳过
FindInBatches 不适合导出,它不是分页器
FindInBatches 设计目标是“分批处理”,不是“分批导出”。它底层依赖 ORDER BY 主键游标,一旦你在 handler 中更新了某行的排序字段(比如 updated_at),后续批次就可能漏数据或重复。
- 导出场景必须自己控制查询逻辑:
SELECT id, name FROM users WHERE id > ? ORDER BY id LIMIT ? - handler 里别调
tx.Save(results)——这会覆盖整批 ID;单条更新请用tx.Model(&r).Where("id = ?", r.ID).Updates(...) -
FindInBatches返回的result.Error永远是nil,错误只能从 handler 的return error捕获,无法用于导出失败中断
真正安全的导出,从来不是选哪个 ORM 方法,而是明确数据流向:数据库 → rows → Scan → 流式写入 → 刷缓冲。任何试图让 GORM “替你扛内存”的做法,在百万行级别都会暴露设计边界。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











