db.Transaction闭包是首选方案,它自动处理panic回滚、成功提交和连接归还;必须全程使用传入的tx实例,禁止混用全局db,否则破坏原子性;嵌套实为savepoint而非新事务;外部调用须置于tx.AfterCommit中。

db.Transaction 闭包是首选,不是可选项
绝大多数业务场景下,直接用 db.Transaction 闭包,而不是手写 db.Begin()。它自动处理 panic 回滚、成功提交、连接归还,避免人为遗漏 Commit() 或 Rollback() 导致连接卡死或数据部分落库。
常见错误现象:tx.Commit() 被跳过(比如分支 return 前没写),后续请求卡在 acquire connection timeout;或者闭包里误调 db.Create()(走的是普通连接),那条数据已不可逆写入,事务原子性彻底失效。
- ✅ 正确:所有 DB 操作都用闭包参数传入的
tx *gorm.DB,例如tx.Create(&order) - ❌ 危险:混用全局
db实例,哪怕只有一处,整个事务就失去原子性 - ⚠️ 注意:
db.Transaction不捕获逻辑错误——只有闭包返回非nilerror 才回滚;分支里忘了return err就会静默丢弃事务
嵌套事务本质是 savepoint,不是新开事务
db.Transaction 闭包里再套一层 db.Transaction,GORM 不会开启新事务,而是创建 SAVEPOINT(MySQL/PostgreSQL 均支持)。标准库和 GORM 都禁止在已有 *sql.Tx 上重复调 Begin(),否则直接 panic:"sql: transaction already in progress"。
所以所谓“嵌套”,只是局部回滚点管理,不是独立生命周期。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
立即学习“go语言免费学习笔记(深入)”;
- ✅ 可行:用
tx.SavePoint("sp_charge")+tx.RollbackTo("sp_charge")(GORM v2.2.5+ 支持) - ❌ 无效:在
db.Transaction闭包里再调一次db.Transaction,外层可能报错或行为未定义 - ⚠️ 注意:savepoint 名称要固定可读(如
"sp_charge"),别用随机字符串,否则调试时无法定位
事务体里不能放 HTTP、Redis、文件等外部调用
任何可能超时、阻塞或失败的外部操作放进事务体,轻则拖慢事务,重则耗尽连接池、引发服务雪崩。GORM 的 tx.AfterCommit() 是唯一安全出口,但它只对当前 tx 生效,且必须在闭包内注册。
- ✅ 正确:HTTP 请求、Redis 写缓存、发 MQ 消息等,统一移到
tx.AfterCommit(func(){})中 - ❌ 危险:在
db.Transaction闭包里直接调http.Post()或rdb.Set() - ⚠️ 注意:
tx.AfterCommit()注册后若事务最终回滚,钩子不会执行;但若事务提交后钩子 panic,不会影响数据库状态,需自行兜底
Repository 层必须支持 WithTx(),不能依赖全局 db
事务上下文不能靠闭包变量或 context “猜”,必须显式契约化。Logic 层决定是否开启事务,Repository 层只负责执行,两者通过 WithTx(*gorm.DB) 明确绑定。如果 Repository 方法内部硬编码用 db,那它永远无法参与上层事务。
- ✅ 正确:Repository 方法接收
tx *gorm.DB参数,或提供WithTx(tx *gorm.DB) *Repo方法链 - ❌ 危险:Repository 里直接引用全局
db,导致事务穿透失败 - ⚠️ 注意:
WithTx必须返回新实例(不可复用原 repo),避免状态污染

















