答案是sql.Stmt执行后不自动释放预编译资源,因其设计要求显式调用Close()才能释放MySQL端的prepared语句槽位;若未关闭,会导致Com_stmt_prepare与Com_stmt_close差值持续增大,最终触发ERROR 1461错误。

为什么 sql.Stmt 执行后不自动释放预编译资源
Go 的 sql.Stmt 是连接池感知的预编译语句句柄,它本身不绑定单个连接,而是复用连接池中的连接执行。调用 db.Prepare() 创建的 *sql.Stmt 不会在 GC 时自动关闭 —— 它会一直占用数据库端的预编译语句槽位(尤其在 MySQL 中有 max_prepared_stmt_count 限制),直到显式调用 stmt.Close()。
常见错误现象:ERROR 1461 (HY000): Can't create more than max_prepared_stmt_count statements,或长期运行服务内存/连接数缓慢上涨。
- 每次
db.Prepare()都向数据库注册一条预编译语句,即使 Go 端已无引用 -
sql.Stmt不实现io.Closer接口,但必须手动Close();不关 ≠ 泄漏,但大概率会泄漏 - 若用
db.Query/Exec直接传 SQL 字符串,底层会走非预编译路径,不触发该问题
如何安全复用 sql.Stmt 而不重复 Prepare
预编译开销主要在首次解析与计划生成,复用 *sql.Stmt 是正确做法,但需注意生命周期管理边界:它不是 goroutine 安全的,不能跨协程并发调用 Query/Exec(会 panic),也不能在 Close 后继续使用。
典型安全模式是「包级变量 + 初始化一次」或「结构体字段 + 在初始化时 prepare」:
立即学习“go语言免费学习笔记(深入)”;
- 全局复用:在
init()或main()开头调用db.Prepare(),赋值给包变量,全程只 prepare 一次 - 避免在 handler 内反复
db.Prepare():比如 HTTP handler 每次都 prepare,等于每请求注册一条新语句 - 若语句参数动态拼接(如
IN (?)参数个数不定),无法复用同一stmt,应改用db.Query("SELECT ... WHERE id IN (?)", args...)—— 此时走非预编译,但更安全
示例:
var getUserStmt *sql.Stmt
func init() {
var err error
getUserStmt, err = db.Prepare("SELECT id, name FROM users WHERE id = ?")
if err != nil {
log.Fatal(err)
}
// 注意:这里不 defer stmt.Close(),因为要长期复用
}
func GetUser(id int) (*User, error) {
row := getUserStmt.QueryRow(id)
// ...
}
sql.Stmt 的 QueryRow 和 Query 对事务的影响
预编译语句本身不开启事务,但一旦绑定到某个 *sql.Tx 上,其所有执行都受限于该事务生命周期。关键点在于:db.Prepare() 返回的 *sql.Stmt 属于连接池,而 tx.Prepare() 返回的 *sql.Stmt 绑定到当前事务,只能在该事务内使用,且事务结束后自动失效(无需手动 Close,但 Close 也不报错)。
- 用
db.Prepare()得到的 stmt 可在任意连接上执行,包括事务内外;但它不能保证跨事务原子性 - 用
tx.Prepare()得到的 stmt 必须用tx.Query/Exec执行,不能混用db.Query,否则 panic 报错sql: statement expects transaction - 事务提交或回滚后,对应
tx.Prepare()的 stmt 自动清理,Go 端再调用会返回sql: statement closed
MySQL 下 max_prepared_stmt_count 超限怎么办
MySQL 默认 max_prepared_stmt_count = 16382,当客户端未 Close 预编译语句,服务端会持续累积,最终拒绝新 Prepare 请求。这不是 Go 的 bug,而是协议层行为 —— MySQL 不主动回收客户端未显式关闭的 stmt。
- 先查当前使用量:
SHOW STATUS LIKE 'Com_stmt_prepare';和SHOW STATUS LIKE 'Com_stmt_close';,差值过大说明 Close 不足 - 线上紧急缓解:重启应用进程(强制释放所有客户端 stmt)或临时调高 MySQL 配置(不推荐长期依赖)
- 根本解法:所有
db.Prepare()必须配对defer stmt.Close()(仅限短期生命周期场景),或明确设计为长生命周期复用(如上文包级变量) - 注意:
sql.DB的SetMaxOpenConns/SetMaxIdleConns不影响预编译语句计数,那是 MySQL server 端独立资源
真正容易被忽略的是:哪怕你用了连接池、启用了 SET GLOBAL max_prepared_stmt_count = 100000,只要 Go 端没 Close,MySQL 就一直记着那条语句 —— 它不因连接断开而自动释放,除非显式 DEALLOCATE PREPARE 或客户端调用 Close。


















