rows.Close() 必须显式调用以释放连接,漏掉会导致连接池耗尽;defer 应在循环结束后、检查 rows.Err() 前调用,不可放在 db.Query() 后立即 defer;rows.Close() 与 db.Close() 职责不同,不可替代。

Rows.Close 忘记调用会导致连接池耗尽
在 Go 的 database/sql 包中,rows.Close() 不只是“清理迭代器”,它实际会把底层的数据库连接释放回连接池。如果漏掉这一步,该连接就一直被 rows 占着,无法复用。
典型表现是:程序运行一段时间后,新查询开始卡住、超时,db.Query() 返回 sql.ErrConnDone 或直接 panic;netstat -an | grep :3306 可见大量 ESTABLISHED 连接堆积;SHOW PROCESSLIST 显示大量 Sleep 状态连接。
-
rows.Next()返回false并不等于连接已释放 —— 必须显式调用rows.Close() - 即使循环提前
break或发生错误退出,也得确保rows.Close()执行(推荐用defer rows.Close()) -
db.QueryRow()内部自动调用Close(),但db.Query()不会
为什么 defer rows.Close() 不能放在 db.Query() 后面就完事
很多人写成这样:
rows, err := db.Query("SELECT ...")
if err != nil {
return err
}
defer rows.Close() // ❌ 错误位置:这里 defer 太早,可能还没读完就关了
for rows.Next() {
// ...
}
问题在于:如果 rows.Next() 中途出错(比如网络断开、类型不匹配),defer rows.Close() 仍会执行,但此时 rows.Err() 还没被检查,错误被掩盖;更糟的是,若 rows.Next() 返回 false 是因为 EOF,rows.Err() 才是最终判据 —— 漏掉它,等于忽略查询过程中的真实错误。
- 正确做法:在
for rows.Next()循环结束后立即调用rows.Close(),再检查rows.Err() -
defer应放在循环之后、函数返回前的最近作用域,例如在for块末尾加一行defer rows.Close() - 注意:
defer在函数 return 时才执行,若函数内有多个 return,要确保每个分支都覆盖到(或统一收口)
Rows.Close 和 db.Close 的关系容易混淆
db.Close() 是关闭整个连接池,而 rows.Close() 是归还单个连接。两者不可替代。
- 只调
db.Close()不关rows:正在使用的连接不会立刻释放,rows对象仍持有连接,直到 GC 或超时强制回收(依赖驱动实现,不可靠) - 只调
rows.Close()不关db:没问题,连接池继续可用;但若整个应用生命周期结束前没调db.Close(),进程退出时操作系统会回收,但测试/热重载场景下可能触发文件描述符泄漏警告 -
rows.Close()被多次调用无害(内部有状态判断),但db.Close()多次调用会 panic
其他类似资源也得配对 Close:Response.Body、File、TCPConn
Go 里凡是实现了 io.Closer 接口的对象,基本都遵循同一套规则:打开就要关,且必须在不再需要时及时关。
-
http.Response.Body:不Close()会导致 TCP 连接无法复用,后续请求变慢甚至失败 -
*os.File:不Close()会占用文件描述符,Linux 默认每进程最多 1024 个,耗尽后open: too many open files -
net.Conn:不Close()会维持 socket 连接,服务端可能长期 hold 连接,触发连接数限制
最稳妥的习惯是:只要看到返回值带 Closer,第一反应就是加 defer xxx.Close(),并确认它在真正用完之后才触发。










