导出大表时必须每调用一次 rows.next() 就立即用 rows.scan() 消费当前行并复用变量,避免内存随行数线性增长导致 oom;同时需正确配置 csv.writer 与 bufio.writer 双缓冲、处理 null 和特殊字符,并通过 setmaxopenconns 限制连接数以防占满数据库连接池。

导出大表时内存爆掉,sql.Rows 必须配合 Scan 流式处理
直接 rows.Columns() 拿完所有字段再 rows.Next() 读取,看似合理,但若没及时 Scan,底层驱动可能缓存整批结果(尤其 MySQL 驱动默认启用 mysql.ParseTime=true 或启用了 multiStatements),导致内存随行数线性增长。导出百万行时很容易 OOM。
正确做法是:每调用一次 rows.Next(),立刻用 rows.Scan() 消费当前行,且变量复用(避免频繁分配):
// 复用切片,长度与列数一致
values := make([]interface{}, len(columns))
valuePtrs := make([]interface{}, len(columns))
for i := range valuePtrs {
valuePtrs[i] = &values[i]
}
<p>for rows.Next() {
if err := rows.Scan(valuePtrs...); err != nil {
return err // 不要忽略 Scan 错误!空值/类型不匹配会在这里暴露
}
// 转为字符串写入 CSV writer
}</p>
-
rows.Scan()是必须的“消费”动作,不调用就等于没读下一行 - 别用
rows.Slice()或rows.Map()类型封装——它们内部仍需 Scan,且额外分配 map/slice - MySQL 驱动中,设置
?readTimeout=30s&writeTimeout=30s&timeout=30s可防长查询卡死连接
CSV 写入性能差,用 csv.Writer + bufio.Writer 双缓冲
直接写文件句柄或 os.Stdout,每次 Write() 都触发系统调用,导出 10 万行可能慢 3–5 倍。Go 标准库的 csv.Writer 本身不带缓冲,必须手动包一层 bufio.Writer。
典型低效写法:csv.NewWriter(f) → 每次 Write() 都落盘;高效写法:
buf := bufio.NewWriterSize(f, 1
- 缓冲区大小建议设为
1(1MB),太小起不到作用,太大无意义(OS 层已有页缓存) -
w.Flush()和buf.Flush()都不能省——前者清空 csv.Writer 的内部 buffer,后者清空 bufio.Writer 的 buffer - 避免在循环里反复
w.Write([]string{...}),改用w.WriteAll(records)批量写(records 是 [][]string)能减少锁竞争
NULL 值和特殊字符让 CSV 解析失败,csv.Writer 默认配置不够用
数据库字段为 NULL 时,Scan 得到的是 nil,若直接传给 csv.Writer.Write() 会 panic;含换行符、双引号、逗号的字符串若未加引号,Excel 或 Python pandas 会解析错行。
标准 csv.Writer 默认行为不足以覆盖这些场景:
w := csv.NewWriter(buf) w.Comma = ',' // 可显式指定分隔符(兼容非英文环境) w.UseCRLF = false // Linux/macOS 用 \n,Windows 用 \r\n(按目标平台选) w.FieldsPerRecord = -1 // 允许每行字段数不同(应对 NULL 导致的缺失)
- 写入前必须将
nil转为""(空字符串),或统一转为"NULL"字符串,不能让interface{}带着nil进Write() - 含双引号的字段会被自动转义(
"a""b"),但前提是字段被包裹在双引号中——csv.Writer默认只对含分隔符/换行符/双引号的字段加引号,无需手动加 - 如果字段含制表符
\t或不可见控制字符,建议提前strings.Map过滤(如func(r rune) rune { if r )
并发导出多张表时连接被占满,sql.DB 的 SetMaxOpenConns 很关键
一个 sql.DB 实例被多个 goroutine 共享时,若没限制最大连接数,导出 10 张表启动 10 个 goroutine,每个都 db.Query(),可能瞬间打满数据库连接池(尤其 MySQL 默认 max_connections=151),后续查询全被拒绝。
必须在初始化 sql.DB 后显式限流:
db, _ := sql.Open("mysql", dsn)
db.SetMaxOpenConns(5) // 最多 5 个活跃连接
db.SetMaxIdleConns(5) // 空闲连接池也设为 5,避免连接泄漏
db.SetConnMaxLifetime(30 * time.Minute)
-
SetMaxOpenConns是硬限制,超过的Query()会阻塞,直到有连接释放——这是保护数据库的底线 - 不要设为 0(不限制)或远大于数据库侧的
max_connections - 如果业务本身已有高并发查询,导出任务应单独用一个
sql.DB实例,并配独立连接池参数
导出逻辑里最容易被跳过的,是 rows.Err() 检查——rows.Next() 返回 false 时,必须紧接着确认 rows.Err() == nil,否则查询中途网络断开、超时等错误会被静默吞掉,你拿到的只是一个截断的 CSV。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











