go 的 sql.rows 本身不支持并发读取,但可通过“顺序读取 + 并发处理”模式高效处理大批量数据(如 20 万行),即单 goroutine 安全遍历结果集,将解析后的结构体分发至多个工作 goroutine 并行处理。
go 的 sql.rows 本身不支持并发读取,但可通过“顺序读取 + 并发处理”模式高效处理大批量数据(如 20 万行),即单 goroutine 安全遍历结果集,将解析后的结构体分发至多个工作 goroutine 并行处理。
在 Go 中,database/sql 包提供的 sql.Rows 是非线程安全的:其 Next() 和 Scan() 方法必须由同一个 goroutine 串行调用,否则会引发 panic 或未定义行为。因此,直接对 rows 启动多个 goroutine 并发调用 Next() 是不可行的。但性能瓶颈通常不在数据库读取本身(I/O 可能已缓冲),而在于后续的业务逻辑处理(如 HTTP 调用、JSON 序列化、复杂计算等)。此时,推荐采用经典的生产者-消费者模型:
- 生产者(1 个 goroutine):主线程(或专用 goroutine)顺序调用 rows.Next() 和 rows.Scan(),将每行数据解析为结构体后发送至带缓冲的通道;
- 消费者(N 个 goroutine):多个工作 goroutine 从同一通道接收数据,并行执行耗时逻辑。
以下是一个健壮、可运行的实现示例:
package main
import (
"database/sql"
"fmt"
"log"
_ "github.com/lib/pq" // 示例使用 PostgreSQL 驱动
)
type User struct {
ID int64
Name string
Email string
}
// 处理单行数据的业务逻辑(模拟耗时操作)
func processUser(u User) error {
// ✅ 此处可安全执行 HTTP 请求、写入文件、调用外部服务等
fmt.Printf("Processing user %d: %s\n", u.ID, u.Name)
return nil
}
func main() {
// 假设 db 已初始化
// db, err := sql.Open("postgres", "...")
// if err != nil { log.Fatal(err) }
// 1. 执行查询(注意:务必检查 err)
rows, err := db.Query("SELECT id, name, email FROM users LIMIT 200000")
if err != nil {
log.Fatal("Query failed:", err)
}
defer rows.Close() // ⚠️ 关键:确保关闭 rows
// 2. 创建带缓冲的通道,容量建议 ≥ 消费者数量 × 2(避免阻塞生产者)
ch := make(chan User, 1000)
// 3. 启动 N 个消费者 goroutine(例如 4 个)
const workers = 4
for i := 0; i < workers; i++ {
go func() {
for user := range ch {
if err := processUser(user); err != nil {
log.Printf("Error processing user %d: %v", user.ID, err)
}
}
}()
}
// 4. 生产者:单 goroutine 顺序读取并发送
for rows.Next() {
var u User
if err := rows.Scan(&u.ID, &u.Name, &u.Email); err != nil {
log.Printf("Scan error: %v", err)
continue // 或 break + rows.Close()
}
ch <- u // 发送至通道
}
// 5. 检查 rows.Err() —— 忽略此步可能导致静默错误!
if err := rows.Err(); err != nil {
log.Fatal("Rows iteration error:", err)
}
// 6. 关闭通道,通知所有消费者退出
close(ch)
}关键注意事项:
- ✅ 必须调用 rows.Close():即使使用 defer,也要确保其作用域覆盖整个迭代过程;
- ✅ 务必检查 rows.Err():rows.Next() 返回 false 后,需显式检查是否有底层 I/O 错误;
- ✅ 通道应显式 close():消费者 goroutine 使用 range ch 依赖通道关闭来退出,否则会永久阻塞;
- ⚠️ 避免通道无缓冲且消费者过慢:若处理速度远低于读取速度,缓冲区过小会导致生产者阻塞,拖慢整体吞吐;建议根据内存与延迟权衡设置缓冲大小(如 make(chan User, 500));
- ? 结构体字段需导出(首字母大写):sql.Scan 依赖反射,仅能设置导出字段;
- ? 如需结果聚合(如统计、写入新表):可在消费者中通过 sync.WaitGroup 或额外通道收集结果,但需注意并发写入共享资源(如 map、slice)时加锁。
该模式在实践中已被广泛验证:它既遵守了 database/sql 的并发约束,又充分释放了多核 CPU 的处理能力,是处理海量 SQL 结果集的标准范式。


















