recover 不能自动回滚事务或修复连接,仅停止 panic 展开并获取 panic 值;必须在事务函数内立即 defer recover,显式调用 rollback 并记录错误,捕获堆栈日志,且 recover 后禁用该 tx 实例。

recover 不能自动回滚数据库事务,也不能修复已释放的连接或已关闭的句柄。它只负责停止 panic 的栈展开、拿到 panic 值,并让当前 goroutine 继续执行——仅此而已。
为什么 defer + recover 必须套在事务函数内部
数据库操作出错(比如空指针解引用、Scan 时类型不匹配、未检查 err 就直接用 rows)触发 panic,若没在事务入口处设 defer,panic 会一路向上,跳过 tx.Rollback(),导致事务悬挂甚至连接泄漏。
- 必须在开启事务后的第一个函数里立即
defer匿名函数,且该函数内调用recover() - 不能把
recover放在 handler 层(如 HTTP middleware),那只能捕获 handler 自身 panic,对tx.QueryRow().Scan(&v)这类底层 panic 无效 - 如果用了 ORM(如 GORM),确认其错误是否真由 panic 抛出——多数 ORM 返回
error,不是 panic;只有极少数场景(如未初始化 DB 实例就调用)才会 panic
recover 后必须显式 Rollback,不能依赖 defer 的顺序
很多人误以为 “只要 defer tx.Rollback() 写在前面,panic 时它就会自动执行”,但这是错的:panic 发生后,只有被 defer 的函数才保证执行,而 tx.Rollback() 是一个普通调用,不是 defer 语句,不会自动触发。
- 正确写法是:在
recover分支里手动调用tx.Rollback() - rollback 失败也要记录日志(
if rbErr != nil { log.Printf("rollback failed: %v", rbErr) }),否则你以为回滚了,其实锁还挂着 - 不要在
recover里重试或重连数据库——这属于业务逻辑,应交由上层决定;recover 只做“止损”
recover 捕获到的 panic 值怎么处理才不丢上下文
直接 fmt.Sprint(r) 只能得到 panic 字符串,比如 "runtime error: invalid memory address",没有行号、无调用栈,根本没法定位问题。
- 必须导入
runtime/debug,用debug.Stack()获取完整堆栈 - 日志至少包含:
panic value+stack trace+tx ID / request ID(如有) - 避免在
recover函数里做耗时操作:写磁盘日志、发 HTTP 上报、调用外部服务——goroutine 会卡住,后续请求堆积 - 如果用了 Sentry 或 Prometheus,建议只发轻量指标(如计数器
panic_total{op="transfer"} 1),堆栈留本地日志
goroutine 之间 recover 不共享,每个 DB 操作要独立防护
一个 HTTP handler 启了 3 个 goroutine 并发查库,其中一个 panic,其他两个不会受影响——但如果你只在 handler 层 defer recover,那个 panic 的 goroutine 仍会崩溃退出,且它的 tx 不会被 rollback。
- 每个启动 goroutine 的地方,都要有自己的
defer func() { recover() }() - 尤其注意
go tx.QueryRow(...).Scan(...)这类写法:Scan 可能 panic,但 go routine 里没 recover,整个 goroutine 就静默死了 - 别指望全局
recover拦住所有 DB panic:main 函数里的全局 recover 只对 main goroutine 有效,对子 goroutine 无效
最易被忽略的一点:recover 后程序继续运行,但 tx 对象已处于不可用状态(无论是否 rollback 成功),后续任何 tx.Query 都会 panic。必须确保 recover 后不再使用该 tx 实例——这点靠代码审查和静态检查很难发现,得靠测试覆盖 panic 路径。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











