CI中运行go test前必须清理GOBIN和GOROOT缓存,因复用环境会导致模块缓存污染、临时二进制冲突及老版本工具干扰;需执行go clean -modcache -testcache并设GOBIN为临时路径。

CI中用go test跑测试前必须清理GOBIN和GOROOT缓存
很多CI流水线在第二次运行go test时突然失败,错误类似cannot load github.com/xxx: cannot find module providing package,根本原因不是代码问题,而是Go模块缓存被污染。CI环境常复用容器或工作目录,go mod download缓存、go build生成的临时_test二进制、甚至GOBIN里残留的老版本工具(比如gofmt)都会干扰测试行为。
- 每次运行前加
go clean -modcache -testcache,强制清空模块和测试缓存 - 设置
GOBIN为临时路径,例如export GOBIN=$(mktemp -d)/bin,避免与系统或前次构建冲突 - 不依赖
GOPATH,统一用go mod模式,且确保go.mod文件存在并已go mod tidy过
并发测试要限制-p参数,否则CI节点容易OOM
本地开发时go test -p 0(默认全核并发)很爽,但在CI里可能让8核机器瞬间拉满内存,尤其当测试含大量http.Server或os/exec子进程时。Kubernetes节点或共享Runner常因OOM被杀,日志只显示Killed,无堆栈。
- CI中固定用
go test -p 2,保守但稳定;若需提速,可按CPU数动态设,如go test -p $(nproc --all)但上限设为4 - 对集成测试(如启动DB、HTTP服务)显式加
-timeout 30s,防止卡死阻塞整个流水线 - 避免在
TestMain里全局启动长期资源,改用testify/suite或t.Cleanup()按需管理
覆盖报告要合并多包结果,go test -coverprofile不能只跑单个目录
直接go test ./... -coverprofile=coverage.out看似合理,但Go会为每个包生成独立的coverage.out,最终只有一个包的结果被保留。CI平台(如Codecov、Coveralls)收不到完整覆盖率,显示“0%”或只统计main包。
go list -f '{{if len .TestGoFiles}}"go test -covermode=count -coverprofile=cover-{{.ImportPath | replace "/" "-"}}.out {{.ImportPath}}"{{end}}' ./... | sh
gocovmerge cover-*.out > coverage.out
rm cover-*.out
- 用
go list遍历所有含测试文件的包,逐个生成cover-xxx.out - 必须用
-covermode=count(而非atomic),否则gocovmerge无法合并 -
gocovmerge需提前go install github.com/axw/gocov/...@latest,注意它不支持Go 1.22+的原生go tool cov格式
失败测试要保留go test -v输出和pprof快照
CI里偶发失败(flaky test)最难排查,日志只显示FAIL: TestXXX (0.12s),没有堆栈或状态。靠重跑几乎无效,因为环境已销毁。
立即学习“go语言免费学习笔记(深入)”;
- 始终加
-v参数:即使用go test -v ./... -timeout 60s,确保每个子测试的t.Log和失败位置可见 - 对疑似死锁或慢测试,加
-cpuprofile=cpu.pprof -memprofile=mem.pprof,失败后自动上传这些文件到CI产物 - 在
TestMain里注册signal.Notify捕获os.Interrupt,让Ctrl+C也能触发runtime/pprof.WriteHeapProfile,方便本地复现
真正麻烦的不是写对配置,而是不同Go版本对go test的并发策略、缓存机制、覆盖统计逻辑有细微差异——CI镜像升级Go小版本前,务必验证go test -race和go test -bench是否仍按预期工作。










