Go 的 sql.Tx 需显式 Commit 或 Rollback,否则连接被占用;不支持嵌套事务和 savepoint;BeginTx 的 context 控制连接生命周期;Rollback/Commit 后 tx 不可再用。

事务没提交或回滚,连接就卡住
Go 的 sql.Tx 不是自动管理生命周期的——它不会因为函数返回、变量离开作用域就自动 Commit 或 Rollback。一旦你调用 db.Begin() 拿到一个 *sql.Tx,后续所有操作都绑定在这个事务上,且必须显式结束;否则连接会一直被占着,直到超时或进程退出。
常见错误现象:database is closed 看似是连接关闭了,其实是连接池耗尽后新请求拿不到连接;context deadline exceeded 也常是事务长期未结束导致后续查询排队阻塞。
- 必须在 defer 中配对处理:先判断
err是否非空,再决定调用tx.Rollback()还是tx.Commit() - 不能只在成功路径写
tx.Commit(),也不能把tx.Rollback()放在if err != nil分支末尾就完事——万一中间 panic 了,还是漏掉 - 推荐写法:用
defer func()匿名函数包裹 rollback,并在 commit 成功后用tx = nil标记已提交,避免重复 rollback
tx, err := db.Begin()
if err != nil {
return err
}
defer func() {
if tx != nil {
tx.Rollback()
}
}()
// ... 执行查询/更新
if err := tx.Commit(); err != nil {
return err
}
tx = nil // 标记已提交,防止 defer 中 rollback
嵌套事务在 Go sql 包里根本不存在
Go 标准库 database/sql 没有嵌套事务支持,tx.Begin() 是非法调用(会 panic),db.Begin() 总是新建一个底层连接级事务。所谓“嵌套”,实际是开发者自己模拟的逻辑分组,底层仍是扁平事务。
使用场景:比如一个服务方法内部调用多个 DAO 函数,每个 DAO 都想“自己管自己的事务”,但又希望整体一致——这时不能靠多次 Begin,而要由最外层统一控制事务生命周期。
立即学习“go语言免费学习笔记(深入)”;
- DAO 层函数应接受
*sql.Tx参数,而不是自己调用db.Begin() - 如果某个 DAO 必须独立提交(如日志写入不依赖主流程成败),应改用
db而非tx,并明确注释“此操作不在当前事务中” - 想实现保存点(savepoint)?标准库不支持,得靠数据库方言(如 PostgreSQL 的
SAVEPOINT+ROLLBACK TO),且需手拼 SQL,无法用*sql.Tx封装
Context 传给 Begin 会影响事务生命周期
db.BeginTx(ctx, opts) 中的 ctx 不只是控制超时,它会绑定到整个事务的底层连接上。一旦 ctx 被 cancel 或超时,该事务关联的连接可能被中断,后续任何 tx.Query、tx.Commit 都会立即返回错误(如 context canceled)。
性能影响:如果传入的是带长 timeout 的 context(比如 30s),但事务本身 200ms 就结束了,连接不会立刻归还连接池——它得等到 context 到期或显式 cancel 才能释放。
- 生产环境建议用短且明确的 context:比如
ctx, cancel := context.WithTimeout(ctx, 5*time.Second),并在事务结束后立刻cancel() - 不要把 HTTP request context 直接传给
BeginTx——它可能长达几十秒,远超事务实际需要时间 - 如果事务内要调用外部 HTTP 服务,应该用另一个独立 context 控制那个调用,别让外部延迟拖垮事务连接
Rollback 后继续用同一个 *sql.Tx 会 panic
*sql.Tx 是一次性对象:一旦调用过 Commit() 或 Rollback(),它就进入终态。再对它调用 Query、Exec 等方法,会直接 panic:sql: transaction has already been committed or rolled back。
容易踩的坑是:在 error 处理分支里 rollback 了,但没 return,后续代码仍尝试用这个 tx;或者 defer 里的 rollback 和手动 rollback 冲突,造成二次 rollback。
- rollback 后务必 return 或用其他控制流跳出当前作用域
- 避免在 defer 之外手动 rollback —— 除非你能 100% 确保不会走到 defer
- 检查所有可能提前 return 的路径(包括 error early return、panic recover 后的逻辑),确保 tx 状态始终可控
事务不是魔法,它只是把多个操作绑到一次连接上并延迟提交。真正复杂的是业务一致性边界——比如扣款和发消息要不要同事务?这得看最终一致性能否接受,而不是看能不能写进一个 tx.Commit() 里。










