t.Cleanup不能替代tx.Rollback,因其仅清理外部资源,不感知SQL事务状态;事务回滚必须显式调用tx.Rollback(),否则可能panic或数据不一致。

t.Cleanup 不是事务回滚的替代品,它只管“测试结束时删东西”,不管数据库有没有提交或回滚。事务回滚必须由 tx.Rollback() 显式触发,t.Cleanup 只能清理它知道的外部资源(比如临时文件、mock server),不能感知 SQL 层状态。
为什么 t.Cleanup 不能代替 tx.Rollback
事务回滚是数据库连接层的语义操作,依赖 *sql.Tx 对象的生命周期和显式调用;而 t.Cleanup 是测试框架在 *testing.T 返回后统一执行的无参函数,两者作用域、触发时机、错误处理机制完全不同。把 tx.Rollback() 塞进 t.Cleanup 看似“自动”,实则危险:
- 如果测试中途
tx.Commit()成功,t.Cleanup里再调tx.Rollback()会 panic:「sql: transaction has already been committed or rolled back」 -
t.Cleanup函数里不能用t.Fatal(),所以 rollback 失败只能t.Log(),你根本收不到告警 - 事务对象可能在测试函数返回前就已关闭(比如 defer
tx.Close()),t.Cleanup拿到的是已失效的指针
正确配对:事务控制 + 资源清理要分层写
一个典型测试中,你要同时管理两类资源:数据库事务(需 commit/rollback)和外部临时资源(如临时目录、mock DB 文件)。它们必须分开处理,且顺序明确:
- 事务逻辑写在测试主流程里,用
if err != nil { tx.Rollback(); t.Fatal(err) }或defer func() { if r := recover(); r != nil { tx.Rollback() } }()保证出错回滚 - 外部资源(比如
os.MkdirTemp创建的目录、sqlite.Open的内存 DB 文件)才交给t.Cleanup - 如果事务本身依赖临时文件(例如 SQLite 内存 DB 实际写到了磁盘临时路径),先注册
t.Cleanup删文件,再开事务——避免 cleanup 先删、事务失败后残留
示例:
立即学习“go语言免费学习笔记(深入)”;
func TestWithDBAndTempDir(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "test.db")
t.Cleanup(func() { os.Remove(dbPath) }) // ✅ 先注册文件清理
<pre class="brush:php;toolbar:false;">db, err := sql.Open("sqlite3", dbPath)
if err != nil {
t.Fatal(err)
}
defer db.Close()
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
// ... 执行 SQL
if err := tx.Commit(); err != nil {
tx.Rollback() // ✅ 主动 rollback,不是靠 cleanup
t.Fatal(err)
}}
哪些临时资源必须用 t.Cleanup 绑定事务上下文
不是所有和数据库沾边的东西都适合 t.Cleanup,只有那些「跨测试可见、不随事务自动释放」的才需要:
-
t.TempDir()创建的目录(含 SQLite 文件、CSV 导出临时目录)→ 必须t.Cleanup(os.RemoveAll) - 启动的嵌入式 DB 进程(如
pgxpool测试用的本地 PostgreSQL)→ 用t.Cleanup发 SIGTERM 并 wait - 修改的全局变量或环境变量(如
os.Setenv("DB_URL", ...))→t.Cleanup恢复原值 - 但
*sql.Tx、*sql.DB、连接池本身不用t.Cleanup关闭——它们该用defer db.Close()或由测试框架管理
容易被忽略的坑:Cleanup 里删文件前没校验路径安全性
t.Cleanup 注册的删除逻辑一旦写错路径,可能误删系统文件。尤其当临时路径来自非 t.TempDir() 的拼接结果时:
- 禁止直接用
os.CreateTemp("", "...")后硬删file.Name()—— 应该先filepath.Abs(file.Name()),再检查是否以os.TempDir()开头 - Windows 下删 SQLite 文件常因句柄未释放报
ERROR_SHARING_VIOLATION,建议加简单重试:for i := 0; i - 如果事务里启了 goroutine 写日志到临时目录,
t.Cleanup可能删到正在写的文件 → 改用os.RemoveAll前先os.Chmod(dir, 0755)确保可写可删
真正难的不是写 cleanup,而是判断「这个资源到底该不该由它管」——事务状态归 SQL 层,文件系统状态归 t.Cleanup,混在一起只会让失败更难定位。


















