推荐用 sql.Rows 配合 bufio.Writer 边查边写:声明变量传地址给 rows.Scan() 避免 panic,每千行 flush 一次,禁用无缓冲的 fmt.Fprintln 直写磁盘。

用 sql.Rows 配合 bufio.Writer 边查边写,别全量加载
Go 里直接调 rows.Scan() 循环读取 + fmt.Fprintln() 或 writer.WriteString() 写文件是最常用也最安全的流式方案。关键不是“怎么写”,而是避免把几百万行结果全塞进 []map[string]interface{} 或 [][]interface{}——那会 OOM。sql.Rows 本身是游标式迭代器,只要不调 rows.SliceScan() 或自己 collect 到 slice,就不会额外占内存。
- 务必在循环前声明变量(如
var id int64; var name string),再传地址给rows.Scan(&id, &name),否则 panic: "sql: Scan error on column index 0: destination not a pointer" - 每写一批(比如 1000 行)调一次
writer.Flush(),防缓冲区积压或程序崩溃时丢数据 - 别用
fmt.Fprintln(file, ...)直接写磁盘文件——它内部没缓冲,小数据高频写性能极差;bufio.NewWriterSize(file, 1(1MB 缓冲)更稳
database/sql 查询必须带 FOR UPDATE 或禁用事务?
不需要。流式导出本质是只读查询,rows.Next() 过程中数据库连接保持活跃,但只要不显式开启事务(db.Begin()),默认就是 autocommit 模式。PostgreSQL/MySQL 都支持长生命周期的只读游标,只要连接不断、SQL 不含 ORDER BY + LIMIT 导致优化器物化结果集就行。
- MySQL 用户注意:如果用了
sql_mode=STRICT_TRANS_TABLES且字段有 NULL,Scan()会报"sql: Scan error on column index X: unsupported Scan, storing driver.Value type <nil> into type *string"</nil>—— 改用sql.NullString等类型接收 - PostgreSQL 用户若用
pgx驱动,可考虑rows.FetchRow()+row.Values(),比标准库Scan()少一层反射开销 - 绝对不要在循环里反复
db.Query()分页查——那是伪流式,网络和锁开销翻倍
写 CSV 文件时怎么处理换行、逗号、双引号
别手写 CSV 字符串。用 encoding/csv.Writer,它自动转义:"a,b" → "\"a,b\"","foo\nbar" → "\"foo\nbar\""。手动拼接几乎必出错,尤其当字段含 Excel 兼容的特殊字符(如 =, +, @ 开头)时还可能触发公式注入。
- 创建 writer 时设
w.Comma = ';'可切分隔符(比如法语 Excel 常用分号) - 调
w.Write([]string{...})写一行,不是fmt.Fprintf;每行末尾自动加\n - 写完必须
w.Error()检查是否写入失败(例如磁盘满),w.Flush()不等于成功
大结果集下如何控制内存与超时
真正卡住的不是 Go 程序,而是数据库连接被长时间占用,或客户端缓冲区撑爆。两个硬控制点:数据库侧加 SET SESSION statement_timeout = '300s'(PostgreSQL)或 MAX_EXECUTION_TIME(300000)(MySQL 5.7+),应用侧用 context.WithTimeout() 包裹 db.QueryContext()。
立即学习“go语言免费学习笔记(深入)”;
-
rows.Close()必须 defer,否则连接不会归还连接池,跑几次就"sql: database is closed" - 如果导出逻辑嵌在 HTTP handler 里,别让
http.ResponseWriter直接当io.Writer—— 客户端断连时容易 panic;先写临时文件,再http.ServeFile() - 日志里记下首行和末行 ID(如
SELECT id, ... FROM t WHERE id BETWEEN ? AND ?),方便断点续传
strace -e trace=write,sendto 看 Go 进程是否持续发 write() 调用,而不是一两秒内集中刷完。


















