TestMain 必须定义在 _test.go 文件中且签名严格为 func TestMain(m testing.M),否则 go test 会静默跳过测试;初始化放 m.Run() 前,清理放 m.Run() 后并显式调用 os.Exit(m.Run()),避免 defer 导致清理过早或退出码丢失。

TestMain 必须定义在 *_test.go 里且签名严格匹配
Go 测试框架只认 func TestMain(m *testing.M) 这一种签名。写成 func TestMain(m testing.M)(漏指针)、func TestMain(m *testing.M) error(多返回值)、或函数名拼错(如 testmain),编译不报错,但 go test 会静默跳过所有测试——你看到的 ok 是假象。
它还必须放在 *_test.go 文件中,不能写在 main.go 或普通 .go 文件里;整个包只能有一个,否则编译报错:multiple definition of TestMain。
初始化放 m.Run() 前,清理必须写在 m.Run() 后
defer 在 TestMain 函数退出时才执行,而 m.Run() 返回后函数还没退出,所以 defer cleanup() 不会在所有测试结束后触发,而是立刻执行——这会导致清理过早,比如 mock server 被提前关掉,后续测试失败。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 正确顺序:初始化 →
m.Run()→ 清理 →os.Exit(code) - 常见错误:把
srv.Close()或db.Close()放在defer里,又没在m.Run()后再调一次 - 更危险的情况:初始化失败(如端口被占、DB 连不上),得提前
os.Exit(1),但已分配的资源(如已创建的临时目录)没清理——得手动加defer os.RemoveAll(tmpDir)配合显式清理逻辑
别在 TestMain 里做非幂等或共享状态操作
多个 go test 并发运行(如 go test ./... -p=4)时,每个包独立跑自己的 TestMain,但同一包内不会重入。问题在于:如果你在里面写死路径、监听固定端口、改 http.DefaultClient,就可能与其他测试冲突。
- 用
os.MkdirTemp("", "test-*")替代os.MkdirAll("/tmp/test", 0755) - 用
net.Listen("tcp", "127.0.0.1:0")让系统分配空闲端口,再取l.Addr().(*net.TCPAddr).Port - 避免全局日志设置:
log.SetOutput改成局部log.New(os.Stderr, "", 0) - 环境变量设置要用
os.Setenv+defer os.Unsetenv,但注意子进程会继承,测试间不隔离
TestMain 返回值必须透传给 os.Exit()
m.Run() 返回整型退出码(0 表示测试全通过,非 0 表示失败)。不调用 os.Exit(code),进程不会终止,go test 会挂起或返回默认成功码,CI 可能误判。
- 错误写法:
return code(Go 不允许从 void 函数返回 int)、os.Exit(0)(忽略真实测试结果) - 正确写法:
os.Exit(m.Run()),且必须是最后一句(或至少在所有清理之后) - 如果初始化失败要提前退出,记得先清理已分配资源,再
os.Exit(1),否则 CI 环境可能残留临时文件或端口
真正容易被忽略的是:TestMain 的生命周期覆盖整个测试进程,但它不感知单个测试的 panic 或超时;m.Run() 返回后,所有 TestXxx 已结束,但它们内部启动的 goroutine 可能还在跑——清理逻辑不能依赖“此时一切已停”,得自己确保同步或超时等待。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










