db.Transaction是默认安全选择,因其内置panic捕获、自动回滚与提交、复用同一连接上下文,屏蔽了手动管理事务时易漏的panic、error返回、tx复用、defer误执行等边界情况。

直接用 db.Transaction,别自己写 Begin + recover —— 它已经内置 panic 捕获和自动回滚逻辑,手动实现极易漏掉边界情况。
为什么 db.Transaction 是默认安全选择
它不是语法糖,而是一个封装了完整生命周期的事务执行器:内部自动调用 Begin,在函数体 panic 时捕获并调用 Rollback,在函数返回 nil 时调用 Commit,且全程复用同一连接上下文。
常见错误是以为“自己 recover 就够了”,但实际要同时处理:panic、return err、return nil、tx 被意外复用、defer tx.Rollback() 在已 Commit 后执行等——db.Transaction 全部屏蔽了这些细节。
- 它只对传入函数体内的 panic 做捕获,不拦截
os.Exit()或runtime.Goexit() - 它不校验业务逻辑错误(比如忘记
return err),所以函数末尾必须显式return nil或return someErr - 函数内所有 DB 操作必须使用参数
tx *gorm.DB,混用原始db会绕过事务
tx.Rollback().Error 不等于“回滚失败”,要检查具体错误类型
事务回滚本身可能成功,但 tx.Rollback().Error 返回非 nil 并不罕见。例如 MySQL 在事务已提交后再调用 Rollback(),会返回 "sql: transaction has already been committed or rolled back";PostgreSQL 可能返回 "pq: current transaction is aborted" —— 这些都是预期行为,不代表数据损坏。
- 真正需警惕的是底层连接错误,如
"i/o timeout"、"broken pipe"、"connection refused" - 若回滚报错且伴随网络异常,应记录日志并触发告警,而非简单忽略
- 不要在
db.Transaction外层再包一层recover,它已做该事;重复捕获反而掩盖真实 panic 来源
手动 Begin 仅用于保存点(SavePoint)等底层控制
当你需要局部回滚(比如订单创建失败但用户积分已扣减,只想回滚订单部分),才考虑 tx.SavePoint("sp1") + tx.RollbackTo("sp1")。此时必须注意:
- 保存点名不能重复,否则后一次
SavePoint会覆盖前一次,导致RollbackTo找不到目标 -
RollbackTo不会释放连接,外层仍需最终Commit()或Rollback() - MySQL 中在保存点内执行
CREATE TABLE等 DDL 语句,会隐式提交整个事务,破坏原子性 - 避免在保存点内调用 HTTP、Redis、文件 IO 等阻塞操作,它们不在事务保护范围内
最易被忽略的一点:事务中所有数据库操作必须严格使用同一个 tx 实例。哪怕只是查一条配置、记一条日志,只要用了原始 db,就脱离了事务上下文——这种“半事务”状态既难调试,又无法通过测试覆盖。











