Go事务回滚必须显式调用tx.Rollback(),不调用不会自动回滚;defer中先设Rollback再成功时Commit;Rollback()出错只需记录日志,不可忽略或向上抛错;database/sql不支持嵌套事务或savepoint。

Go 事务回滚必须显式调用 tx.Rollback(),不调用就不会回滚
Go 的 database/sql 中,事务是手动控制的:开启后,除非你主动调用 tx.Commit() 或 tx.Rollback(),否则连接会一直持有锁、事务状态持续,直到连接被关闭(此时数据库可能自动回滚,但行为不可靠且不一致)。很多新手误以为“函数退出就自动回滚”,结果发现数据已提交或连接卡死。
常见错误现象:sql: transaction has already been committed or rolled back —— 本质是重复调用了 Commit() 或 Rollback();或者压根没调任何操作,事务悬而未决。
- 务必在
defer里配对使用:先defer tx.Rollback(),再在成功路径上tx.Commit()并return - 不能只靠
if err != nil分支调用Rollback(),因为中间可能 panic,或逻辑提前 return -
Rollback()本身可能返回 error(比如网络断开),但它不影响“是否已回滚”这一事实 —— 回滚动作通常已由数据库执行,error 只表示无法确认结果
tx.Rollback() 返回 error 时该怎么处理
tx.Rollback() 的 error 不代表回滚失败,而是代表“无法获知回滚是否成功”。比如数据库连接已断,Go 侧发不出请求,自然拿不到响应。这时候你再重试 rollback 没有意义,也不能抛出 panic,更不该忽略。
典型使用场景:微服务中事务失败需记录日志并通知监控,但不能阻塞主流程。
立即学习“go语言免费学习笔记(深入)”;
- 总是检查
err,但仅用于日志:if err := tx.Rollback(); err != nil { log.Printf("rollback failed: %v", err) } - 不要用
if err != nil { return err }向上层传递 —— 这会让调用方误以为“回滚失败导致业务异常”,实际业务状态已是回滚完毕 - 避免在
Rollback()后继续操作tx,它已进入 final 状态,再调用任何方法都会返回sql: Transaction has already been committed or rolled back
嵌套事务和 Savepoint 在 Go 里不存在,别被名字误导
Go 标准库 database/sql 完全不支持嵌套事务或 savepoint。所谓“子事务”只是业务逻辑分段,底层仍是单个 *sql.Tx。有人尝试用两个 Begin() 套着用,结果第二个直接 panic:sql: transaction already in progress。
如果你需要局部回滚(比如批量插入中某条失败,不影响前面的),只能自己模拟:
- 拆成多个独立事务(最稳妥,但失去原子性)
- 在内存中预校验 + 构建完整 SQL 批量执行,失败则整体跳过
- 依赖数据库原生 savepoint(如 PostgreSQL):用
tx.Exec("SAVEPOINT sp1")和tx.Exec("ROLLBACK TO sp1"),但要注意这属于方言,MySQL 5.7- 不支持,SQLite 支持有限,且database/sql不做抽象,需手写 SQL 字符串
事务超时或上下文取消时,tx.Rollback() 仍要调用
当用 db.BeginTx(ctx, nil) 带上下文开启事务,若 ctx 超时或被取消,数据库连接不会自动回滚事务 —— Go 驱动最多中断当前正在执行的语句,但事务本身还开着。后续如果复用该连接,可能遇到隔离级别混乱或锁等待。
所以即使 ctx.Err() != nil,也要确保走到 Rollback():
- 用
select { case 不够,因为 <code>ctx.Done()可能早于 SQL 执行完成 - 正确做法:把整个事务逻辑包进带 cancel 的 goroutine?不推荐。更简单的是——所有路径都走统一 cleanup,包括 defer rollback + 显式 commit/rollback 判断
- 注意:调用
tx.Rollback()时若ctx已取消,某些驱动(如pgx)会立即返回 cancel error,但仍会向数据库发 rollback 请求;标准lib/pq则可能阻塞,建议设置db.SetConnMaxLifetime()配合连接池健康检查
事务真正难的不是写那两行 Rollback() 和 Commit(),而是想清楚“哪段逻辑必须原子”“失败时哪些资源要清理”“context 取消后连接是否还能安全复用”。这些没法靠语法糖解决。










