db.Transaction会自动捕获panic并回滚事务,但仅限于其传入的匿名函数体内;对os.Exit、Goexit、tx.AfterCommit内panic、手动Commit后panic、被自定义recover吞掉的panic均无效。

db.Transaction 会自动捕获 panic 并回滚
绝大多数业务场景下,db.Transaction 就是你要的答案。它内部用 recover() 捕获闭包内任何未处理的 panic,并在 panic 发生时自动调用 tx.Rollback() —— 你不用手动 defer 或写 recover。
但要注意:它只对传入的匿名函数体生效,panic 必须发生在该函数内部;如果 panic 出现在 tx.AfterCommit() 钩子里,或函数返回后、事务已提交的阶段,它就管不到了。
-
db.Transaction不捕获os.Exit()、runtime.Goexit()这类强制退出,事务会挂起 - 若你在函数里手动调了
tx.Commit(),再 panic,GORM 不会二次回滚(连接已释放) - panic 被你自己的
defer + recover捕获并吞掉,db.Transaction就完全感知不到
手写 Begin/Commit 时 panic 回滚必须自己加 defer
如果你非要用 db.Begin()(比如需要控制超时或自定义 session),那 panic 回滚就得自己兜底。GORM 不会帮你做这件事。
典型写法是用 defer 包一层 recover,并在 panic 时触发 tx.Rollback():
tx := db.Begin()
if tx.Error != nil {
return tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
// ... 业务逻辑
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return err
}
- 别把
tx.Commit()放在 defer 里——它会在所有路径都执行,导致成功后又 rollback -
tx.Rollback()可安全重复调用,但tx.Commit()多次会 panic 报 “invalid transaction” - 如果用了 context,优先用
db.BeginTx(ctx, &sql.TxOptions{}),超时会自动中断事务,避免悬挂
panic 回滚不等于业务错误回滚
这是最容易被忽略的一点:db.Transaction 对 panic 回滚很可靠,但它完全不管逻辑错误。比如你忘了 return err,或者分支里写了 return(无值),函数静默结束,事务既不 commit 也不 rollback,连接直接归还池中丢弃——数据没写,也没报错。
- 所有数据库操作必须用传入的
tx,混用db.Create()会绕过事务直连库 -
tx提交或回滚后不可复用,再调tx.Create()会 panic 报 “invalid transaction” - HTTP 请求、Redis 写入、日志上报等 I/O 操作严禁放进事务体,否则 panic 可能掩盖更严重的阻塞问题
嵌套 panic 场景下 savepoint 不会自动回滚
如果你在 db.Transaction 里又调 tx.SavePoint("sp"),然后在 savepoint 内 panic,GORM 不会自动 RollbackTo("sp") —— 它只做最外层事务的 rollback,savepoint 的局部回滚必须你自己显式写。
也就是说,panic 触发的是整个事务回滚,不是“回退到最近 savepoint”。
- savepoint 名称要固定可读(如
"sp_payment"),别随机生成,否则调试时无法定位 -
tx.SavePoint("sp")和原生tx.Exec("SAVEPOINT sp")混用容易报 “no such savepoint” - 真正需要局部回滚时,优先考虑拆成独立事务 + 幂等重试,而不是依赖 savepoint


















