inuse长期高位且idle归零即连接泄漏;openconnections持续上涨、waitcount跳变需立即排查;pprof搜conn.begin、rows.next/close、tx.commit/rollback、conn.waitclosed定位卡点。

看 db.Stats() 里 InUse 和 Idle 的异常组合
连接没释放最直接的信号不是报错,而是 db.Stats() 返回的两个数字“说人话”:如果 InUse 长期卡在高位(比如 QPS 20,InUse 却稳定在 150+),同时 Idle 不增反降甚至归零,基本就是连接被占着不放。这不是“可能漏了”,是已经泄漏。
别等服务崩——只要 OpenConnections 持续上涨、WaitCount 开始跳变,就得立刻查。
-
InUse > 实际并发峰值 × 2是强预警信号(例如压测 30 QPS,InUse却停在 100+) -
Idle == 0且OpenConnections == MaxOpenConns→ 池被撑满,新请求排队 -
WaitDuration > 0且持续增长 → 请求已在阻塞,根源大概率是旧连接未 Close
pprof 堆栈里搜这四个关键词
访问 curl "http://localhost:6060/debug/pprof/goroutine?debug=2",用 Ctrl+F 搜下面这些字符串,能快速定位卡点:
-
database/sql.(*DB).conn或conn.begin→ goroutine 卡在获取连接,说明上一个连接没归还 -
(*Rows).Next或(*Rows).Close→ 正在遍历但没走到rows.Close(),或defer rows.Close()写错位置 -
(*Tx).Commit或(*Tx).Rollback缺失 → error 分支漏掉 rollback,事务一直 hold 连接 -
select.*conn.waitClosed→ 连接已被标记关闭,但还有 goroutine 在等它,通常是Close()被提前或重复调用
三类高危写法,静态检查根本发现不了
这些代码跑起来才暴露,review 很难 catch,必须靠运行时观察 + pprof 验证:
-
rows, err := db.Query(...)后只判err != nil就 return,没加if rows != nil { defer rows.Close() }—— Query 失败时rows仍可能非 nil 且持有连接 - for 循环里反复调
db.QueryRow(),却把defer rows.Close()写在循环外 → 只有最后一次被关,前 N−1 次全泄漏 - DAO 层返回
func() (*User, error)这种闭包,内部启了*sql.Tx却没暴露 rollback 接口 → 调用方根本不知道要关啥
db.QueryRow() 和 db.Query() 的 Close 义务完全不同
db.QueryRow() 返回的是单行结果,它不返回 *sql.Rows,所以不需要 Close();但 db.Query() 必须显式 rows.Close(),哪怕你只读一行或 rows.Next() 返回 false。
- 误以为
QueryRow().Scan()成功就完事 → 安全,不用 Close - 误以为
Query().Next()走完就自动释放 → 错,rows.Close()必须手动调,否则连接永远卡住 - 用
defer rows.Close()但函数中间 return → defer 不会执行,连接泄漏 - 正确姿势:
if rows != nil { defer func() { _ = rows.Close() }() },并确保rows作用域覆盖整个使用路径
*sql.Rows 或 *sql.Tx 逃出当前函数作用域,或者被封装进闭包/返回值,释放责任就模糊了,泄漏几乎必然发生。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











