db.Transaction 是 GORM 唯一可靠自动回滚方式,它内部开启事务、执行闭包、依返回值提交或回滚,并自动 recover panic;手动 Begin() 无兜底,需自行处理错误、panic 和连接释放,嵌套 Transaction() 会导致脱离上下文而失效。

db.Transaction 是 GORM 实现自动回滚的唯一可靠方式,其他写法(比如 defer tx.Rollback() 或嵌套调用 Transaction())都不能保证正确性。
db.Transaction 为什么能自动回滚
它内部做了三件事:开启事务、执行闭包、根据返回值决定提交或回滚,并且会 recover() panic。你不用管连接释放、错误判断、panic 捕获——全由它兜底。
- 闭包里任何地方返回非
nilerror,自动触发Rollback() - 闭包里发生 panic,自动
recover并Rollback() - 闭包成功返回
nil,才调用Commit() - 所有数据库操作必须使用传入的
tx *gorm.DB,不能混用原始db
err := db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&order).Error; err != nil {
return err // 自动回滚
}
if err := tx.Create(&payment).Error; err != nil {
return err // 自动回滚
}
return nil // 自动提交
})
手动 Begin() 不会自动回滚
tx := db.Begin() 后,GORM 完全不干预后续流程。你漏掉 Commit() 或 Rollback(),连接就卡住;没 recover(),panic 会导致连接泄漏;用 defer tx.Rollback() 更危险——成功时也会误回滚。
- 必须显式检查每个操作的
Error,按需调tx.Rollback() - 必须自己加
defer func() { if r := recover(); r != nil { tx.Rollback() } }() -
tx.Commit()失败时,仍要再调一次tx.Rollback()(否则状态残留)
嵌套事务不是真嵌套,Transaction() 也不能套娃
MySQL 不支持真正嵌套事务,GORM 的“嵌套”只是基于 SavePoint 的模拟。但 db.Transaction(func() error { db.Transaction(...) }) 是错的:第二层 Transaction() 会脱离外层上下文,变成独立事务,数据提前落库。
- 真正可嵌套的只有
tx1.Begin()→tx2 := tx1.Begin()这种链式调用 -
tx2.Commit()实际只是打一个保存点标记,不是提交 - 子事务失败要用
tx2.Rollback()回到tx2起点,不影响tx1已做操作 - 最终成败仍由
tx1.Commit()或tx1.Rollback()决定
自动回滚失效的典型场景
- 在
Transaction()闭包里调了未传tx的db.Create():操作跑默认连接池,不参与事务 - 闭包里启动 goroutine 并在其中用
tx:goroutine 可能比闭包先结束,tx被回收或复用 - 把日志、HTTP 请求、文件写入等非 DB 操作放在
Transaction()里:延长事务时间,增加死锁风险,且这些操作无法回滚 - 用
tx.SavePoint("sp")但名字重复,导致RollbackTo()跳过中间状态
最常被忽略的一点:自动回滚只对闭包内显式返回的 error 或 panic 生效。如果 error 被吞掉(比如只打日志没 return),事务就会照常提交。











