goroutine 中直接复用全局 *sql.DB 安全,但需正确配置连接池参数、所有调用带超时 context、显式关闭 rows、事务不可跨 goroutine 使用。

goroutine 中直接复用 sql.DB 是安全的,但必须避免手动管理连接
Go 的 database/sql 包本身已内置连接池,sql.DB 实例是并发安全的,可被任意数量的 goroutine 同时调用 Query、Exec 等方法。不需要、也不应该为每个 goroutine 新建一个 sql.DB 或尝试“复用连接对象”。常见错误是误以为要自己维护连接生命周期,结果导致连接泄漏或 panic。
正确做法是:全局初始化一个 *sql.DB,设置好连接池参数,之后所有 goroutine 直接使用它。
-
db.SetMaxOpenConns(n)控制最大打开连接数(含正在使用 + 空闲),设太小会排队阻塞,设太大可能压垮数据库 -
db.SetMaxIdleConns(n)控制空闲连接上限,建议 ≤MaxOpenConns,否则空闲连接不会被回收 -
db.SetConnMaxLifetime(d)强制连接在存活时间后关闭(推荐 5–30 分钟),防止因网络中断或数据库侧超时导致 stale connection -
db.SetConnMaxIdleTime(d)(Go 1.15+)控制空闲连接最长保留时间,避免长期空闲后被中间件或防火墙断连
事务操作不能跨 goroutine,sql.Tx 不是并发安全的
一旦调用 db.Begin() 得到 *sql.Tx,该事务对象只能由创建它的 goroutine 使用。把它传给其他 goroutine 并发执行 tx.Query 或 tx.Exec 会导致 panic 或数据不一致 —— 因为 sql.Tx 内部持有单个底层连接,且未加锁保护。
常见误用场景:启动多个 goroutine 并行写入,每个都试图复用同一个 tx。
立即学习“go语言免费学习笔记(深入)”;
- 若需并行写入且保证原子性,应由主 goroutine 统一收集数据,再用单个事务批量提交
- 若各子任务逻辑独立、无需事务强一致性,直接用
db.Exec等非事务接口,让连接池自动分配连接 - 若必须分阶段提交(如先写 A 表再写 B 表),可在每个 goroutine 内各自开启短事务,但需自行处理失败重试与幂等性
长时间运行的 goroutine 可能阻塞连接池,引发 context deadline exceeded
当某个 goroutine 执行耗时 SQL(如未加 limit 的全表扫描、大字段读取、无索引 join)且未设 context 超时,它会独占一个连接较久,导致后续请求在 db.GetConn 阶段等待,最终触发 context.DeadlineExceeded 错误。这不是数据库挂了,而是连接池被拖满。
解决核心是:所有数据库调用必须带带超时的 context.Context。
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
rows, err := db.QueryContext(ctx, "SELECT * FROM users WHERE status = ?", status)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
log.Println("query timed out")
}
return err
}
- 不要用
db.Query/db.Exec等无 context 版本,它们默认无超时 - HTTP handler 中优先用
r.Context(),而非context.Background() - 对写操作,考虑用
context.WithCancel在业务逻辑中主动中断(如用户取消上传)
连接泄漏通常源于忘记调用 rows.Close() 或未处理 defer 作用域
调用 db.Query 或 db.QueryRow 返回的 *sql.Rows 或 *sql.Row 必须显式关闭,否则底层连接不会归还给连接池。常见疏漏点:
- 在 for 循环内
rows.Next()出错后直接 return,忘了rows.Close() - 用
defer rows.Close()但 defer 写在错误检查之前,导致rows == nil时 panic - 把
rows传给另一个函数处理,但调用方没关,接收方也没关
推荐写法:
rows, err := db.QueryContext(ctx, "SELECT id, name FROM posts")
if err != nil {
return err
}
defer rows.Close() // 安全:rows 非 nil
for rows.Next() {
var id int
var name string
if err := rows.Scan(&id, &name); err != nil {
return err
}
// 处理数据
}
if err := rows.Err(); err != nil {
return err
}
真正难排查的是那些隐式持有连接的场景:比如 ORM 封装、自定义 scanner、或第三方库内部未关闭 Rows。遇到连接数缓慢上涨,优先检查所有 Query 调用点是否配对了 Close。










