数据库连接池应在进程退出前全局关闭,而非在中间件中调用db.close(),否则会导致后续所有请求panic;中间件只需确保事务正确提交或回滚,并防止goroutine泄漏和连接耗尽。

中间件里不能直接关数据库连接
中间件的生命周期绑定在单次 HTTP 请求上,db.Close() 是全局资源释放操作,一旦调用,后续所有请求都会 panic 报 sql: database is closed。这不是“优雅关闭”,是直接拔电源。
数据库连接池本身不需要中间件干预
Go 的 sql.DB 是连接池抽象,不是单个连接;它内部自动复用、回收、健康检查连接。你只需在进程退出前调一次 db.Close(),让池等所有活跃连接归还后再返回——这个动作必须放在 http.Server.Shutdown() 之后、main 函数退出前,而不是塞进中间件。
- 中间件里唯一该做的,是确保每个请求使用的
*sql.Tx或*sql.Conn被显式Commit()/Rollback()或Close() - 漏掉
tx.Rollback()(比如 defer 写错成tx.Commit())会导致连接卡在“in transaction”状态,阻塞db.Close() - 用
db.BeginTx(ctx, nil)时,务必传入 shutdown 阶段仍在生效的 context,否则事务可能 hang 住不响应取消
中间件中真正要防的是“连接泄漏”
典型泄漏场景不是连接关不掉,而是连接永远拿不出去、也还不回去。比如:
- 中间件里启了 goroutine 去查 DB,但没传 cancel channel,shutdown 时 goroutine 还在等
db.QueryRowContext()返回 - 用了
db.SetMaxOpenConns(1)却在中间件里并发发起多个查询,后面请求永远阻塞在acquireConn - 中间件调用了第三方 SDK(如某监控埋点),它内部悄悄开了新
sql.DB实例却没暴露 Close 接口
验证方法:压测时 curl -s http://localhost:8080/debug/pprof/goroutine?debug=2 | grep Query,看是否有大量 goroutine 卡在 database/sql.(*DB).queryDC —— 那就是泄漏信号。
Shutdown 阶段怎么确保 DB 真关干净
别只依赖 defer db.Close()。生产环境要主动等:
- 在
srv.Shutdown(ctx)返回后,立刻调db.Close() - 加一个短超时等待(比如 3 秒),用
select { case 避免卡死 - 如果日志里频繁出现
waiting for database connections to be returned,说明业务代码里有地方忘了rows.Close()或tx.Rollback()
最常被忽略的一点:GORM 用户以为 gorm.DB.Close() 就完事了,其实它底层包装的 *sql.DB 才是真身,必须拿到那个实例再关——GORM v2 不再透出原生 sql.DB,得用 db.DB() 方法取出来。











