绝大多数业务写操作必须用db.transaction而非手写begin/commit,因db.begin()返回新事务实例tx,混用db会绕过事务直连数据库;tx提交或回滚后不可复用;db.transaction仅捕获panic并回滚,不校验逻辑错误、不自动提交,易静默失败;http等i/o操作严禁入事务体。

绝大多数业务写操作,必须用 db.Transaction,而不是手写 db.Begin() / tx.Commit();否则极大概率漏回滚、卡连接、数据不一致。
为什么 db.Begin() 后继续用 db.Create() 就失效了
因为 db.Begin() 返回的是一个全新的 *gorm.DB 实例(带事务上下文),原始 db 对象仍是无事务的普通连接。所有 CRUD 操作若继续走 db,就等于绕过事务直连数据库。
- ✅ 正确:用返回的
tx调用tx.Create()、tx.Update()、tx.Preload().First() - ❌ 危险:在
tx事务中混用db.Create()—— 这条记录已不可逆落库,破坏原子性 - ⚠️ 注意:
tx提交或回滚后不可复用,再调tx.Create()会 panic 报"invalid transaction"
db.Transaction 为什么没报错却没提交
db.Transaction 只执行你传入的函数,不检查返回值、也不自动提交。如果函数里忘了 tx.Commit(),又没显式 tx.Rollback(),事务在连接归还给池时就静默丢弃——数据既没写进去,也没回滚,连错误都没有。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 必须在事务函数开头加
defer tx.Rollback()做兜底 - 成功路径上要显式调用
tx.Commit(),不能靠return nil自动提交 - 别在中间裸
return,改用命名返回值或统一出口:err := ...; if err != nil { return err } - 函数内分支遗漏
return err(比如条件判断后直接return无值),事务会静默丢弃
HTTP 调用、缓存写入、日志上报能放进事务里吗
不能。任何可能阻塞或超时的 I/O 操作放进事务,轻则拖垮连接池,重则导致整个服务不可用。GORM 本身不提供事务级超时控制,context.WithTimeout 传给 db.Transaction 只能中断函数执行,不会自动 rollback。
- HTTP 请求、Redis 写入、Kafka 发送、本地日志等,必须移出事务函数体
- 推荐做法:在
tx.AfterCommit(func() { ... })中注册后续动作,但钩子里别做耗时操作,建议发消息到chan或投递异步任务 -
tx.AfterCommit必须在db.Transaction函数体内、tx创建后立刻注册,且严格绑定当前实例;用tx.Session()或tx.WithContext()新建的*gorm.DB会丢失钩子
嵌套事务其实是 SAVEPOINT,不是真正子事务
GORM 的 tx.Begin() 在已有事务内调用,底层是数据库 SAVEPOINT,MySQL 和 PostgreSQL 行为一致,但非真正嵌套事务。
- 统一用
tx.SavePoint("sp_name")和tx.RollbackTo("sp_name"),别混用原生 SQL 的SAVEPOINT sp,否则容易报"no such savepoint" -
outer.Rollback()会连带清除所有 savepoint,inner状态作废;inner.RollbackTo("sp")只回滚到保存点,外层已执行的操作不受影响 - 除非业务强依赖局部回滚,否则优先拆成独立事务 + 幂等重试,比 savepoint 更可控
- 避免跨函数传递 savepoint 名,容易命名冲突;建议用
uuid.NewString()动态生成
事务最危险的不是报错,而是静默失败——没提交、没回滚、没提示、数据状态未知。关键在于每一步都明确控制权归属:谁创建 tx,谁负责 Commit 或 Rollback,谁触发 AfterCommit,以及哪些操作根本不能出现在事务体内。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










