Go事务必须显式Commit或Rollback,否则连接卡死、数据悬而未决;不调用会导致连接池耗尽,重复调用会报错“transaction has already been committed or rolled back”。

Go 事务不是“开了就自动管到底”,必须手动 Commit 或 Rollback,否则连接卡死、数据悬而未决——这是最常被忽略的前提。
为什么 tx.Commit() 和 tx.Rollback() 必须成对显式调用
database/sql 的 sql.Tx 不会自动提交或回滚。函数退出、goroutine 结束、甚至 panic 都不会触发隐式回滚。不调 tx.Commit(),事务一直 hold 着连接和锁;不调 tx.Rollback(),可能造成连接池耗尽、后续请求卡在 acquire connection timeout。
- 常见错误现象:
sql: transaction has already been committed or rolled back—— 本质是重复调用了 Commit/Rollback,或压根没调任何一者就让tx变量超出作用域 - 正确姿势:用
defer先注册tx.Rollback(),再在成功路径上显式tx.Commit()并return -
tx.Rollback()返回error时,只应记录日志,不可向上返回或 panic。它只表示“无法确认回滚结果”,不代表回滚未发生
BeginTx(context.Context, *sql.TxOptions) 是唯一安全起点
用 db.Begin() 开启的事务完全脱离 context 控制:超时、取消信号对其无效。真正能绑定生命周期的只有 db.BeginTx(ctx, opts)。
-
ctx在BeginTx时即绑定到底层连接,后续所有tx.Query()、tx.Exec()都隐式复用它——这些方法签名里没有context.Context参数,别试图传tx.QueryContext()(该方法不存在) - 若
ctx已超时或取消,BeginTx直接返回 error,根本不会创建tx;但已创建的tx不会因ctx后续取消而中断正在执行的 SQL(数据库侧需另配statement_timeout) - 混用
db.Query()和tx.Exec()会导致部分操作绕过事务上下文,破坏原子性
所有 SQL 操作必须走 tx 对象,禁用 db.Xxx()
事务失效的头号原因:db.BeginTx() 后,仍用原始 *sql.DB 调用 db.Exec()、db.QueryRow() 等——这些操作走的是连接池里的独立连接,与 tx 完全无关。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
立即学习“go语言免费学习笔记(深入)”;
- 必须全部改用
tx.Exec()、tx.QueryRow()、tx.Prepare(),哪怕只是查一条配置也要走tx -
sql.Tx不是线程安全的,禁止跨 goroutine 复用;若需并发执行,应拆分为多个独立事务,或用 channel 协作 - PostgreSQL/MySQL 8.0+ 支持 savepoint,但标准库无封装,得手写
tx.Exec("SAVEPOINT sp1")和tx.Exec("ROLLBACK TO SAVEPOINT sp1"),名字需自行去重(如加 uuid 前缀)
GORM 场景下别信“类型兼容”就等于进了事务
很多 GORM 生成代码或封装函数接收 Querier 接口,你传 *sql.Tx 能编译通过,但实际执行可能偷偷 new 出一个 *gorm.DB 实例,导致操作脱离事务。
- 务必检查生成函数内部是否真用了传入的
querier,有些模板会忽略参数、直接调用全局db - 推荐做法:定义
type Repo struct { q Querier },初始化时注入db或tx,所有 SQL 方法都从r.q调用 - 嵌套事务在 GORM 中是 savepoint 模拟,
inner.Rollback()只回退到保存点,外层仍可继续;但outer.Rollback()会一并清除所有 savepoint
事务真正的复杂点不在怎么开启,而在「边界是否清晰」:SQL 执行路径有没有被中间件、日志、监控或 ORM 封装悄悄绕开 tx。每多一层抽象,就要多一次验证——别假设接口兼容就等于语义一致。

















