GORM事务失败后不检查.Error会导致静默提交或数据不一致。所有写操作错误藏在.Error字段,忽略它会使失败事务被误提交;Transaction闭包需显式返回err才能触发回滚;Commit()失败也必须检查其.Error;嵌套事务需透传内层错误至外层。

事务失败后不抛出错误,GORM 就会静默提交或继续用已失效的事务对象,数据不一致风险极高。
事务操作后忽略 Error 字段的典型后果
很多人写 tx.Create(&user) 后没接 .Error,以为“没报 panic 就成功了”。实际上:
- GORM 所有写操作(
Create、Save、Update、Delete)都返回*gorm.DB,错误藏在.Error字段里,不是 panic 也不是返回值 - 若忽略
.Error,后续仍调用tx.Commit()—— 此时 GORM 会尝试提交一个内部已标记失败的事务,PostgreSQL/MySQL 可能直接忽略或报错但被吞掉 - 更隐蔽的是:某些驱动(如
pgx)在事务失败后继续执行语句会返回sql.ErrTxDone,但如果你没检查,它就变成 nil 错误,Commit()看似成功,实则什么都没写入
db.Transaction 闭包里 return nil 却没触发回滚?
闭包事务看似自动,但前提是:你必须显式 return err。常见误写:
err := db.Transaction(func(tx *gorm.DB) error {
tx.Create(&u1) // ❌ 忘记 .Error
tx.Create(&u2) // ❌ 同上
return nil // ✅ 闭包认为“一切正常”,自动 Commit
})
这会导致两个问题:
- 哪怕
u1因唯一键冲突插入失败,tx.Create(&u1).Error是非 nil,但你没读它,就等于放弃失败信号 - 闭包内所有操作都失去原子性约束,
u2可能被插入,而u1没插成 —— 违反事务初衷 - 正确写法是每步都检查:
if err := tx.Create(&u1).Error; err != nil { return err }
Commit() 失败却检查旧错误变量
这是线上最常被忽视的逻辑断点。看这段代码:
tx := db.Begin()
tx.Create(&u)
err := tx.Create(&v).Error // ← 记录 v 的错误
if err != nil {
tx.Rollback()
return
}
tx.Commit() // ← 但这里可能失败!
// ❌ 错误:没检查 tx.Commit().Error
后果很直接:
- 假设
v插入成功,但Commit()因网络中断、锁超时或主从同步延迟失败 - 你只检查了
err(即v的插入错误),却放过了Commit()的失败 - 结果:日志显示“操作完成”,数据库里查不到
u和v,且无任何报错提示 - 修复很简单:
if err := tx.Commit().Error; err != nil { tx.Rollback(); return err }
嵌套事务中外层未感知内层失败
用 tx.Begin() 做保存点(SavePoint)时,内层事务失败但外层没处理,会导致部分回滚失效:
outer := db.Begin() inner := outer.Begin() inner.Create(&log) // 成功 inner.Create(&order) // 失败,比如库存不足 // ❌ 错误:只 rollback inner,没通知 outer inner.Rollback() // outer.Commit() 仍会执行 → log 被提交,order 缺失 → 数据不一致
关键点在于:
-
inner.Rollback()只撤销保存点之后的操作,不影响outer的状态 - 但业务上
log和order是强关联的,order失败理应连带丢弃log - 必须由内层把错误透传出去:
if err := inner.Commit().Error; err != nil { outer.Rollback(); return err }
事务不是“写了 Begin 就安全了”,它的可靠性完全依赖于每一处 .Error 是否被读取和响应。漏掉任何一个,就等于在数据一致性上开了个口子——它不会立刻崩,但会在高并发、网络抖动或异常分支里突然暴露。


















