go test 中 sql.tx 手动回滚不生效,主因是混用 tx 与 db 连接、事务非并发安全跨测试复用、defer 前 panic 导致回滚未执行、未适配驱动对 savepoint 的支持,以及连接池泄漏。

Go test 中用 sql.Tx 手动回滚为什么有时不生效
事务回滚失败,往往不是因为没调用 tx.Rollback(),而是测试代码里误用了连接。比如在 tx 上执行查询后,又拿 db *sql.DB 直接查——这俩走的是不同连接,tx 的变更对 db 不可见,但 db 的写入也不会进 tx,导致回滚像没发生一样。
- 所有数据库操作必须统一走
tx对象,包括tx.Query()、tx.Exec(),不能混用db.Query() - 注意
tx.Stmt()返回的语句也绑定在该事务上,可复用但不可跨tx - 如果用
database/sql驱动(如pgx或mysql),需确认驱动支持嵌套事务或 savepoint;否则多次Begin()可能静默降级为普通连接
用 testify/suite 管理事务测试时的生命周期陷阱
很多人把 tx 存在 TestSuite 字段里,SetupTest() 开事务、TeardownTest() 回滚,结果多个测试并行跑时出错——sql.Tx 不是并发安全的,且 TeardownTest() 可能被调度到别的 goroutine,导致回滚发生在其他测试的事务上。
-
tx必须在每个测试函数内部创建和销毁,不要跨测试复用或缓存 - 若用
testify/suite,在SetupTest()里只初始化共享依赖(如*sql.DB),事务逻辑收进每个TestXxx()函数体内 - 避免在
defer tx.Rollback()前 panic 或提前 return,否则回滚不触发;可用if tx != nil { tx.Rollback() }显式兜底
PostgreSQL 中 SAVEPOINT 比 ROLLBACK 更适合单元测试
纯 tx.Rollback() 虽然干净,但每次都要重连+建表+插初始数据,太慢。PostgreSQL 支持 SAVEPOINT,能在单个事务内划出可撤回的段,既隔离测试数据,又省去反复启停事务的开销。
- 在测试前用
tx.Exec("SAVEPOINT test_sp")打点,测试末尾用tx.Exec("ROLLBACK TO SAVEPOINT test_sp") - 注意
SAVEPOINT名称要唯一,建议用t.Name()拼接,防止子测试嵌套冲突 - MySQL 8.0+ 也支持
SAVEPOINT,但老版本或 SQLite 需降级为全事务回滚;用前先查驱动文档是否透出该能力
测试数据库连接池泄漏导致事务卡死
本地跑单个测试没事,CI 上偶发超时或 context deadline exceeded,大概率是 sql.DB 连接没释放完,后续 Begin() 被阻塞在池队列里——尤其在 defer 写错位置或 panic 没 recover 时。
立即学习“go语言免费学习笔记(深入)”;
- 每个测试结束后显式调用
db.Close()(仅限测试用的临时 db 实例,非全局复用) - 检查是否在
defer里写了tx.Rollback()却忘了tx本身没 Close:虽然sql.Tx没Close()方法,但它的底层连接会在Commit()或Rollback()后归还池中 - 用
db.Stats().OpenConnections在测试前后打日志,确认连接数回归基线值
事务回滚真正难的不是语法,是控制边界:谁持有连接、谁决定生命周期、哪条语句属于哪个上下文。一旦跨了 goroutine、测试函数或 driver 层抽象,就容易漏掉一环。










