事务回滚失败主因是事务对象复用,正确做法是用db.Begin()获取独立tx实例并全程使用,出错立即Rollback、成功才Commit;GORM不支持嵌套事务,需用SavePoint实现局部回滚;务必defer Rollback防连接泄漏。

事务回滚失败的典型表现
执行 db.Rollback() 后数据仍被写入数据库,或程序 panic 报错 sql: transaction has already been committed or rolled back —— 这通常不是 GORM 的 bug,而是事务对象生命周期管理出错。GORM 的事务是基于 *gorm.DB 实例的,一旦调用 Commit() 或 Rollback(),该实例就失效,不能再复用。
正确开启并控制事务的三步法
必须用新生成的事务 DB 实例操作,且只在该实例上做增删改查。常见错误是把全局 db 和事务 tx 混用。
- 调用
db.Begin()获取事务实例tx,它是一个独立的*gorm.DB - 所有数据库操作(
Create、Save、Update等)都必须用tx调用,不能用原始db - 仅在逻辑成功时调用
tx.Commit();任一环节出错(包括校验失败),立即调用tx.Rollback()
示例:
tx := db.Begin()
defer func() {
if r := recover(); r != nil {
tx.Rollback()
}
}()
if err := tx.Create(&user).Error; err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&profile).Error; err != nil {
tx.Rollback()
return err
}
return tx.Commit().Error
嵌套事务与 SavePoint 的实际取舍
GORM 不支持真正的嵌套事务,Begin() 在已有事务中调用会静默返回当前事务(即“扁平化”),不会新建隔离层。若需局部回滚,得显式用 SavePoint:
立即学习“go语言免费学习笔记(深入)”;
-
tx.SavePoint("sp1")设置保存点,返回一个可回滚到该点的子事务对象 - 后续操作可用该子事务实例,失败时调用
tx.RollbackTo("sp1") -
RollbackTo不会结束整个事务,之后仍可继续Commit或其他Rollback - 注意:MySQL 5.7+、PostgreSQL 支持,SQLite 需启用
PRAGMA journal_mode = WAL
事务超时与连接泄漏的隐性风险
未显式 Commit 或 Rollback 的事务会一直占用数据库连接,尤其在 HTTP handler 中忘记 defer 处理时,容易触发连接池耗尽(报错 connection refused 或卡死)。GORM 本身不自动超时回滚。
- 务必对每个
Begin()配对defer tx.Rollback(),再在成功路径中覆盖为Commit() - 避免在事务内做 HTTP 请求、文件 IO 等长耗时操作 —— 事务持有时间越长,锁竞争和连接阻塞越严重
- PostgreSQL 用户注意:
ROLLBACK无法撤销已发出的NOTIFY或临时表创建
真正难处理的是跨函数边界的错误传播:一旦某个子函数内部 panic 但没被外层捕获,defer 就不会执行。这时候事务就悬在那里了。


















