绝大多数业务写操作必须用 Transaction 而非 Begin,因其自动提交/回滚、panic 安全;Begin 仅适用于跨函数传事务或手动管理 savepoint 的底层场景。

什么时候必须用 Transaction 而不是 Begin
绝大多数业务写操作——比如「创建订单 + 扣减库存 + 记录日志」——都该用 Transaction。它自动处理提交/回滚,且函数返回非 nil 错误时立刻回滚,不用手动 defer 或 recover。
而 Begin 是底层裸事务,只适合两种情况:需要跨多个函数传递事务对象(如分层调用)、或要手动控制 savepoint / 嵌套回滚点。普通业务逻辑硬套 Begin 容易漏 Commit 或 Rollback,导致连接卡死、数据不一致。
-
Transaction是安全默认项,闭包内 panic 也会被捕获并回滚 -
Begin后若忘记tx.Commit(),连接不会释放,后续请求可能卡在acquire connection timeout - 嵌套事务(inner.Begin())必须显式管理内外层状态,
Transaction不支持嵌套,别试图在闭包里再调一次
Transaction 里不能直接用全局 db 对象
常见错误:在 Transaction 闭包里混用 db.Create() 和 tx.Create()。前者走的是新连接或连接池里的空闲连接,完全不在当前事务上下文中,数据会提前落库,破坏原子性。
所有操作必须用传入的 tx *gorm.DB 实例,它是带事务上下文的专属句柄。
立即学习“go语言免费学习笔记(深入)”;
- ✅ 正确:
tx.Create(&order)、tx.Where("id = ?", id).Updates(&stock) - ❌ 危险:
db.Create(&order)—— 这条记录已不可逆写入 - 查关联数据也一样:
tx.Preload("Items").First(&order),不是db.Preload
性能敏感场景下要不要关掉默认事务
GORM 默认对每个 Create/Update/Delete 都启一个隐式事务(哪怕没调 Transaction),这是为了保证单条语句的 ACID。但如果你明确知道某批操作不需要事务保护(比如写日志表、埋点统计),可以初始化时加 SkipDefaultTransaction: true,实测提升 30%+ 写入吞吐。
- 关掉后,
db.Create()变成直连执行,无事务开销,但失败也不回滚 -
Transaction仍可用,它不受SkipDefaultTransaction影响 - 别全局关闭再在业务里手动补
Begin——这等于自己实现一套易出错的事务调度
嵌套事务不是“事务里再开事务”,而是 savepoint
GORM 的“嵌套事务”本质是 SavePoint:外层事务未提交前,内层可独立回滚到某个保存点,不影响外层。它不是数据库层面真正的嵌套事务(MySQL 本身不支持)。
所以 inner.Rollback() 并不会释放连接,只是回退到 savepoint;只有外层 outer.Commit() 或 outer.Rollback() 才真正结束事务。
- 写法:
inner := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Begin()(需显式 Session) - 内层失败时:
inner.Rollback(),外层继续执行其他逻辑 - 外层仍需显式
outer.Commit(),否则所有变更丢失
这个机制容易被当成“子事务隔离”,结果发现 inner 回滚后,outer 里之前写的其他数据也没了——因为它们本就在同一个事务里,savepoint 只是快照点,不是隔离边界。


















