testing — Go 测试框架完全指南
testing 包是 Go 工具链内置的自动化测试框架,配合 go test 命令使用。本文基线为 go1.27.0 windows/amd64,所有 API 均经 go doc、GOROOT api/go1.2x.txt 或本机实测核实;凡标注"实测"的输出来自真实运行结果。
与旧版手册相比,本文补齐了 Go 1.24~1.27 的大量新入口(b.Loop、t.Context、t.Chdir、testing/synctest、stdversion 检查器等),并纠正了若干过时说法。
1. 测试文件与函数约定
- 测试文件必须以
_test.go结尾,与被测代码同目录;普通构建会排除它们,go test时才会编入(go doc testing)。 - 测试文件可以位于被测包内(可访问未导出成员),也可以位于
包名_test的外部包(只能访问导出 API)。
四种被 go test 自动识别的函数:
| 类型 | 签名 | 识别规则 |
|---|---|---|
| 单元测试 | func TestXxx(t *testing.T) |
Xxx 首字母不能小写 |
| 基准测试 | func BenchmarkXxx(b *testing.B) |
同上,需 -bench 触发 |
| 示例测试 | func ExampleXxx() |
靠 // Output: 注释断言 stdout |
| 模糊测试 | func FuzzXxx(f *testing.F) |
go test 只跑种子语料,-fuzz 才变异 |
TestMain 与 os.Exit 纪律
一个包最多定义一个 func TestMain(m *testing.M),用于包裹整个测试二进制的生命周期(建库、起容器、全局开关)。标准写法来自 go doc testing:
func TestMain(m *testing.M) {
setup()
code := m.Run() // 跑完所有测试,返回退出码
teardown()
os.Exit(code) // 必须把 m.Run() 的结果传给 os.Exit
}三条纪律(均有出处):
- 不要
return代替os.Exit(m.Run())。TestMain 的返回值不会被go test采用;不显式退出,失败会被吞掉。 os.Exit不运行 defer(go doc os.Exit原文:“The program terminates immediately; deferred functions are not run”)。清理逻辑放在m.Run()之后、os.Exit之前,或改用t.Cleanup。- 测试函数中途
os.Exit(0)会 panic。1.27 实测:
func TestCallsExit0(t *testing.T) { os.Exit(0) }panic: unexpected call to os.Exit(0) during test [recovered, repanicked]测试二进制内部有 -test.paniconexit0(默认开启,帮助文本原文 “panic on call to os.Exit(0)")专门拦截"测试里调 os.Exit(0) 假装成功"的行为。注意 TestMain 末尾的 os.Exit(m.Run()) 是文档模式、不受此拦截影响。
子测试 t.Run 与并行
go doc testing.T.Run 原文:“Run runs f as a subtest of t called name. It runs f in a separate goroutine and blocks until f returns or calls t.Parallel”,返回值为"f 是否成功(或至少在调用 t.Parallel 前未失败)"。
- 子测试名会拼接到父测试名:
-run 'TestAdd/positive'单独运行一个用例,-list可列出完整名字。 - 子测试内
t.Parallel()会暂停该子测试,等兄弟都声明/结束后由调度器并发恢复;父测试函数在所有并行子测试完成后才结束(t.Run文档)。 go doc testing.T.Parallel:并行测试只与其他并行测试并发,且会等所有非并行测试结束;配合-count重复运行时,同一测试的多个实例永不互相并行。
func TestParallel(t *testing.T) {
t.Parallel()
t.Cleanup(func() { /* 测试与其全部子测试结束后按后注册先执行调用 */ })
tmpDir := t.TempDir()
t.Setenv("APP_ENV", "test")
}2. 常用 flag 速览
以下均出自 1.27 本机 go help testflag 与测试二进制帮助(go test -args -h 等价视图):
| flag | 作用 |
|---|---|
-run regexp |
只跑名字匹配的测试(父/子 两段正则) |
-list regexp / -skip regexp |
只列出不运行 / 跳过匹配项 |
-v |
实时输出日志与 === RUN/PAUSE/CONT 轨迹 |
-count n |
重复 n 次;帮助原文:Examples 永远只跑一次,且 -count 对 -fuzz 命中的模糊测试不适用。1.27 实测 -count=0:不跑任何测试,输出 testing: warning: no tests to run 后 PASS |
-race |
竞态检测器(见第 5 节末尾"为什么它测不出时序 bug”) |
-shuffle off,on,N |
打乱执行顺序;1.27 实测 -shuffle=on 首行打印 -test.shuffle 1791952529212579300,把该数字传给 -shuffle=1791952529212579300 可复现同一顺序 |
-timeout d |
整个测试二进制的墙钟超时,默认 10m |
-parallel n |
并行测试的最大并发,默认 GOMAXPROCS;只对单个测试二进制生效 |
-bench regexp / -benchtime / -benchmem |
基准三件套,详见第 6 节 |
-fuzz regexp / -fuzztime |
模糊测试开关与预算,详见第 7 节 |
-failfast |
首个失败后不再启动新测试 |
-cpuprofile file |
采集 CPU profile,配合 pprof 分析基准(见 pprof.md) |
注意:1.27 的工具链没有
-subtests这个 flag。实测go test -subtests报flag provided but not defined: -subtests(该名字只存在于测试名里)。想控制子测试粒度,用-run 'TestX/子名'或-list。
3. testing.T 核心方法
3.1 错误报告:Error 与 Fatal
t.Error("unexpected value") // = Log + Fail,继续执行
t.Errorf("got %v, want %v", got, want)
t.Fatal("cannot continue") // = Log + FailNow,终止当前测试
t.Fatalf("init failed: %v", err)
t.Skip("环境不满足") // = Log + SkipNowFailNow 的文档措辞值得整段记住(go doc testing.T.FailNow):
“FailNow marks the function as having failed and stops its execution by calling runtime.Goexit (which then runs all deferred calls in the current goroutine). FailNow must be called from the goroutine running the test or benchmark function, not from other goroutines created during the test. Calling FailNow does not stop those other goroutines.”
三个推论:只能在跑测试函数的那个 goroutine 里调用;Goexit 只执行当前 goroutine 的 defer;其他 goroutine 不会被它杀死。子 goroutine 里想报告失败用 t.Error,想联动退出让主测试 goroutine 轮询 t.Failed()。
选择指南:多个独立断言用 Error(一次跑完收集所有失败);后续步骤依赖此结果用 Fatal。
3.2 Helper / Cleanup / TempDir / Setenv
func assertEqual[T comparable](t *testing.T, got, want T) {
t.Helper() // 必须放第一行:错误行号指向调用方
if got != want {
t.Errorf("got %v, want %v", got, want)
}
}t.Cleanup(f):注册在"该测试及其全部子测试完成后"执行的函数,顺序为后注册先执行(go doc 原文 “last added, first called order”)。t.TempDir():自动创建、测试结束自动删除的目录;每次调用返回唯一目录,创建失败直接Fatal;受GOTMPDIR环境变量支配(go doc 原文)。t.Setenv(key, val):测试结束自动恢复。原文警告:“Because Setenv affects the whole process, it cannot be used in parallel tests or tests with parallel ancestors."——并行测试里调用会 panic(见 4.3 节实测)。t.TempDir/t.Setenv不能与t.Parallel同用这条纪律同样适用于t.Chdir。t.Attr(key, value string)(Go 1.25 新增,api/go1.25.txt#43936):向测试日志发射一条给 CI 系统消费的元数据键值。t.ArtifactDir()(Go 1.26 新增,api/go1.26.txt#71287):返回该测试专属的产物目录,配合go test -artifacts收集输出文件。
4. Go 1.24 三件套:b.Loop / t.Context / t.Chdir
三个方法同时挂在 *T、*B、*F 和 TB 接口上(api/go1.24.txt #62516、#36532、#61515)。
4.1 b.Loop() — 基准循环的现代写法
go doc testing.B.Loop 关键语义(原文提炼):
- 返回 true 表示基准循环应继续;首次调用时自动重置计时器(setup 不计时),返回 false 时自动停止计时(cleanup 不计时)。
- 循环体内的赋值与函数调用结果会被编译器用
runtime.KeepAlive包装,防止整个循环体被优化删除;条件必须恰好写成b.Loop(),这个保活只作用于花括号内语句。 - “Loop offers more automatic management of the benchmark timer, and runs each benchmark function only once per measurement, whereas b.N-based benchmarks must run the benchmark function (and any associated setup and cleanup) several times.”
- 官方要求二选一:“Benchmarks should either use Loop or contain a loop to b.N, but not both.”
- Loop 返回 false 后,
b.N等于实际跑过的总迭代数,可用于计算均值。
var sink []int
func prepare() []int {
data := make([]int, 1000)
for i := range data {
data[i] = i
}
return data
}
func BenchmarkLoop(b *testing.B) {
data := prepare() // 不需要 ResetTimer,首次 b.Loop() 自动清账
b.ReportAllocs()
for b.Loop() {
sink = append(sink, data[len(sink)%1000])
}
}1.27 实测(-benchtime=2000x,i7-10750H,-12 为 GOMAXPROCS):
BenchmarkLoop-12 2000 21.45 ns/op 30 B/op 0 allocs/op
BenchmarkOld-12 2000 23.05 ns/op 34 B/op 0 allocs/op陷阱:
for b.Loop()里不要用 break/return 提前跳出。无条件跳出时框架直接报错(1.27 实测:benchmark.go:225: benchmark function returned without B.Loop() == false (break or return in loop?)→ FAIL);而带条件的 break 甚至可能 PASS——实测if b.N > 1 { break }正常报出 837,974,197 次迭代、1.541 ns/op,但那基本是 break 与循环开销本身,数字没有意义(在 Loop 形式的循环里读b.N本来就不可靠)。两种结局都比"老实跑完 Loop"糟:要么报错,要么产出假数据。
4.2 t.Context() — 随测试取消的 context
go doc testing.T.Context 原文:“Context returns a context that is canceled just before Cleanup-registered functions are called.”
它替代了"手工 ctx, cancel := context.WithCancel(...) + defer cancel()“的样板:挂在它上面的 context.AfterFunc、goroutine 会在测试结束时被自动取消,而 Cleanup 可以等这些依赖 Done 的资源收完尾。典型用法:
func TestWorker(t *testing.T) {
ctx := t.Context() // 不用 WithCancel,测试结束自动 cancel
go worker(ctx)
t.Cleanup(func() {
// 此函数执行时 ctx 已取消,worker 应已退出
t.Logf("cleanup: ctx.Err() = %v", ctx.Err())
})
}1.27 实测:测试运行期间 ctx.Err() 为 nil,cleanup 里打印 cleanup: ctx.Err() = context canceled,阻塞在 <-ctx.Done() 的 goroutine 正常苏醒。与 context 包本身的用法参见 context.md。
4.3 t.Chdir(dir) — 临时切目录并自动切回
go doc testing.T.Chdir 原文:Chdir 调用 os.Chdir 并用 Cleanup 在测试结束后恢复原工作目录;“On Unix, it also sets PWD environment variable for the duration of the test”(Windows 上不设 PWD,实测平台差异按原文理解即可)。同样因为影响整个进程:不能用于并行测试或有并行祖先的测试。
违反的后果不是普通失败,而是整个测试二进制 panic 退出(1.27 实测):
func TestChdirParallel(t *testing.T) {
t.Parallel()
t.Chdir(t.TempDir())
}panic: testing: test using t.Setenv, t.Chdir, or cryptotest.SetGlobalRandom
can not use t.Parallel [recovered, repanicked]
exit status 2注意 panic 文案把 t.Setenv、t.Chdir 和 testing/cryptotest.SetGlobalRandom(Go 1.26 新增,api/go1.26.txt #70942,用于钉住全局随机源)并列为"进程级状态修改器”,三者与 t.Parallel 互斥是同一套 checkParallel 机制。
5. synctest 深潜:确定性并发测试
5.1 版本谱系(逐条核实)
- Go 1.24:
testing/synctest没有公开 API 记录(本机grep -c synctest api/go1.24.txt结果为 0),该实验版不进入稳定 API。 - Go 1.25:
api/go1.25.txt收录pkg testing/synctest, func Test(*testing.T, func(*testing.T))与func Wait()(#67434),稳定形态是 Test/Wait,不是早期 Run——1.27 的go doc testing/synctest.Run明确报 “no symbol Run in package testing/synctest”(Run 只作为内部实现internal/synctest.Run存在于栈里)。 - Go 1.27:新增
synctest.Sleep(time.Duration)(api/go1.27.txt#77169)。
5.2 bubble:一个自世界的并发沙箱
synctest.Test(t, func(t *testing.T){...}) 把 f 放进一个"bubble(气泡)",气泡内启动的所有 goroutine 也属于这个气泡。核心规则(package 文档原文提炼):
- 虚拟时钟:每个气泡有自己的时钟,初始时间固定为 midnight UTC 2000-01-01。“Time in a bubble only advances when every goroutine in the bubble is durably blocked”——时间只在组内 goroutine 全部 durably blocked 时推进;气泡根 goroutine 退出后时间停止推进。
- durably blocked 的精确定义:阻塞且只能被同气泡内其他 goroutine 唤醒才算。会 durably 阻塞的操作:气泡内创建的 channel 收发、所有 case 都是气泡内 channel 的 select、
sync.Cond.Wait、sync.WaitGroup.Wait(Add 也在气泡内时)、time.Sleep。不会 durably 阻塞的:抢锁(Mutex/RWMutex)、网络/文件 IO、系统调用——它们可被气泡外事件唤醒。 - 全员阻塞时的三种结局:调用过
Wait()则 Wait 返回;否则(若根未退出且)存在某个未来时刻能唤醒至少一个 goroutine,虚拟时间直接跳到那个时刻——Sleep/time.After/Ticker因此"秒过”;再否则死锁,Test 触发 panic。 - 隔离:气泡内创建的 channel/timer/ticker 与气泡绑定,从气泡外操作它们会 panic;
sync.WaitGroup在首次Add/Go时绑定气泡。文档特别提示包级变量var wg sync.WaitGroup无法绑定气泡的技术限制(改用new(sync.WaitGroup)指针则无此问题)。 - Test 的收尾语义(
go doc testing/synctest.Test原文):Test 会等气泡内全部 goroutine 退出后才返回;“If the goroutines in the bubble become deadlocked, the test fails”;不允许在气泡内再调 Test;传给 f 的 T 有三条特殊规则——Cleanup 在气泡内、Test 返回前执行;T.Context()的 Done 与气泡关联;T.Run、T.Parallel、T.Deadline不得调用。
5.3 示例一:超时逻辑的确定性验证
传统写法要么真等 1 秒(慢),要么缩短超时(测不到真实配置);synctest 里这 1.5 秒虚拟时间瞬间跑完且完全确定。1.27 实测:
func TestTimeoutBubble(t *testing.T) {
realStart := time.Now()
synctest.Test(t, func(t *testing.T) {
start := time.Now() // 2000-01-01 00:00:00 UTC
ch := make(chan int)
go func() {
time.Sleep(500 * time.Millisecond)
ch <- 42
}()
select {
case v := <-ch:
t.Logf("received %d at virtual +%s", v, time.Since(start))
case <-time.After(1 * time.Second):
t.Log("timeout branch")
}
select {
case v := <-ch:
t.Logf("received %d at virtual +%s", v, time.Since(start))
case <-time.After(1 * time.Second):
t.Logf("timeout branch, virtual total +%s", time.Since(start))
}
})
t.Logf("real elapsed = %s", time.Since(realStart))
}=== RUN TestTimeoutBubble
p_test.go:20: received 42 at virtual +500ms
p_test.go:28: timeout branch, virtual total +1.5s
p_test.go:31: real elapsed = 0s
--- PASS: TestTimeoutBubble (0.00s)第一次 select 必走 500ms 生产者分支(虚拟时间跳到 500ms 唤醒发送方);第二次必走 After 分支。真实墙钟 0 秒——这就是"时序敏感逻辑也能当普通单元测试跑"的形态。
5.4 示例二:goroutine 泄漏会被当场抓住
气泡内的 goroutine 泄漏(永不退出、阻塞在气泡内 channel 上)不会"下次再爆",而是当场死锁 panic。1.27 实测:
func TestLeakedGoroutine(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
ch := make(chan int)
go func() {
v := <-ch // 永远没人发:泄漏这个 goroutine
_ = v
}()
})
}--- FAIL: TestLeakedGoroutine (0.00s)
panic: deadlock: main bubble goroutine has exited but blocked goroutines remain
[recovered, repanicked]
...
goroutine 10 [chan receive (durable), synctest bubble 1]:
p.TestLeakedGoroutine.func1.1()
E:/tmp/probe-sec06/testing/04/p_test.go:12 +0x25panic 前的 goroutine 转储直接点名泄漏者(状态标注 chan receive (durable))。这相当于把"泄漏检测"内置进了调度器:不需要 goleak 式的全量 goroutine 扫描,也不必担心它误报第三方后台 goroutine。
5.5 synctest.Sleep(Go 1.27 新增)
go doc testing/synctest.Sleep 原文:阻塞直到气泡时钟前进 d 且同气泡其他 goroutine 全部 durably blocked,“This is exactly equivalent to time.Sleep(d); synctest.Wait()"。文档解释的动机值得抄下来:如果测试自身和被测 goroutine sleep 了完全相同的时长,哪个先恢复是不可预料的;Sleep 保证测试睡完之后被测方已"settle”。1.27 实测:
func TestSleepSettle(t *testing.T) {
synctest.Test(t, func(t *testing.T) {
done := make(chan struct{})
go func() {
time.Sleep(2 * time.Second)
close(done)
}()
synctest.Sleep(2 * time.Second)
select {
case <-done:
t.Log("worker settled")
default:
t.Log("worker NOT done when Sleep returned")
}
})
}p_test.go:37: worker settled, virtual +2s5.6 为什么 -race 测不出这些
竞态检测器监控的是内存访问的 happens-before 关系:两个 goroutine 对同一块内存、至少一个写、无同步——它能抓 data race,但它不建模"超时/顺序/等待"这类时序不变式,且只有被调度到的那一次运行才有机会暴露问题。synctest 走的是另一条路:把调度点变显式(全员 durably blocked 才动时间),任何依赖真实抢占时序才能复现的 bug,在气泡里要么必然复现(如 5.4 的泄漏)、要么根本不需要碰运气(如 5.3 的 500ms/1s 竞争,结果确定)。package 文档同时给了使用纪律:气泡内避免与外部 goroutine 交互、避免真实网络(要网络就用 net.Pipe 这类假连接——文档的 HTTP 100-Continue 示例就是 net.Pipe 实现的)、避免外部进程、避免后台泄漏。
6. 基准测试进阶
6.1 循环写法:一律迁移到 b.Loop
老式 for i := 0; i < b.N; i++ 仍然是合法写法,但注意 4.1 节的两条官方约束:与 b.Loop() 不可混用;b.N 式基准每次测量调整 N 时都要重跑整个函数(含 setup),而 b.Loop() 每个测量只跑一次函数。setup 昂贵时,b.Loop 自动处理的计时器管理(首次调用重置、返回 false 时停表)比手工 b.ResetTimer() 更难写错。
6.2 运行参数与结果字段
-benchtime支持两种形态(go help testflag原文):时长如-benchtime 1h30s,或迭代次数如-benchtime 100x。默认 1s,框架自动把 N 加大到跑满预算。b.ReportAllocs()等价于在结果里输出B/op与allocs/op两列。- 被测函数每次消费/产出固定字节数时调用
b.SetBytes(int64(n)),结果追加MB/s列。 -count=5以上再看波动;单次测量噪声很大——1.27 实测-count=2时同一基准两次结果可从 23.20 ns/op 跳到 47.35 ns/op(GC 抖动)。跨版本对比建议go run golang.org/x/tools/cmd/benchstat@latest old.txt new.txt。- 并行吞吐基准用
b.RunParallel(func(pb *testing.PB))(go doc 原文:把 b.N 次迭代分给多个 goroutine,数量默认 GOMAXPROCS,非 CPU 密集可用SetParallelism加大,通常配合-cpu使用)。 - 热点函数定位:
go test -bench=. -cpuprofile=cpu.out后看 pprof.md。 - 参数分组基准用
b.Run("size=1024", func(b *testing.B){...}),运行时可-bench 'Name/size'。
6.3 计时之外的坑
基准日志(b.Log)总是会被打印,包括 PASS 时——go doc testing.B 原文解释是"以免隐藏影响基准结果的存在"。所以别指望靠日志静默来省时间;另外 b.Fatal 系同样只能在跑基准函数的 goroutine 里调用。
7. 模糊测试(Fuzzing)
7.1 结构与契约
// parse.go —— 故意埋一个 panic bug,让 fuzzer 去抓
package p
import "strings"
func Parse(s string) string {
if strings.ContainsRune(s, 0) {
panic("unexpected NUL byte")
}
return strings.ToUpper(s)
}// parse_test.go
func FuzzParse(f *testing.F) {
f.Add("hello") // 种子语料:正常/边界/历史 bug 输入
f.Add("")
f.Add("UPPER")
f.Fuzz(func(t *testing.T, s string) {
Parse(s) // 不变式:任何输入都不许 panic
})
}契约(go doc testing.F):Fuzz 测试默认只跑种子语料——包括 f.Add 注册的和 testdata/fuzz/FuzzXxx/ 目录里的文件;跑模糊需要显式 -fuzz=FuzzXxx。*F 的方法只能在 f.Fuzz 之前调用,目标函数内只允许 F.Failed 与 F.Name(go doc 原文),其余一律用 *T。-fuzztime 同样支持时长与 1000x 两种形态;-count 对 -fuzz 命中的目标不适用。
7.2 实测一次完整 fuzz(go1.27.0 windows/amd64)
$ go test -run '^$' -fuzz FuzzParse -fuzztime 20s
fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed
fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 12 workers
fuzz: minimizing 38-byte failing input file
fuzz: elapsed: 0s, minimizing
--- FAIL: FuzzParse (0.47s)
--- FAIL: FuzzParse (0.00s)
testing.go:2076: panic: unexpected NUL byte
p.Parse(...) E:/tmp/probe-sec06/testing/06/parse.go:9
...
Failing input written to testdata\fuzz\FuzzParse\390ca22614d4ce1a
To re-run:
go test -run=FuzzParse/390ca22614d4ce1a
FAIL要点:workers 数默认等于 GOMAXPROCS(本机 12);发现失败输入后引擎会做最小化(minimizing),把 38 字节的原始反例缩到最短;-run '^$' 用于跳过普通测试只跑 fuzz。
落盘的语料文件是可直接阅读的文本格式(实测内容):
go test fuzz v1
string("\x00")7.3 回放、缓存与 CI 语义
- 回归保护:失败语料提交进 git 后,普通
go test就会把它当种子跑——1.27 实测:不加任何 flag 直接go test,输出同样以panic: unexpected NUL byteFAIL 收尾。修复Parse之前这个包在 CI 里永远是红的。 - 定向回放:
go test -run=FuzzParse/390ca22614d4ce1a -v精确重放单条语料(实测输出--- FAIL: FuzzParse/390ca22614d4ce1a)。 - fuzz 缓存:模糊引擎的工作语料(覆盖率反馈中间产物)放在
go env GOCACHE下的fuzz目录;本机实测跑-fuzz之前C:\Users\Willow\AppData\Local\go-build\fuzz不存在,跑完之后出现并按目标包建了p/子目录。测试二进制内部对应-test.fuzzcachedirflag,日常用默认即可。 - CI 默认行为:
go test不开模糊(这是设计,不是缺陷)——CI 上跑的是"种子语料回归";主动变异是开发者本地的探索模式。想持续变异要用专门的 fuzz 编排(如 OSS-Fuzz 思路),别把-fuzz塞进 PR 流水线。
8. go vet 与测试相关的新检查器
8.1 stdversion(Go 1.27 进入 go vet 默认集)
go tool vet help 在 1.27 GOROOT 中的登记原文:stdversion report uses of too-new standard library symbols。官方 1.27 发布说明将其列为本版 go vet 新增检查,且**go test 默认 vet 集也包含它**(go.dev Go 1.27 Release Notes,本机 1.27.0 实测吻合)。它解决的问题:module 的 go 指令声明了低版本,代码却用了高版本才加入的标准库符号——过去这类不匹配要等低版本用户编译时才炸。
实测(go.mod 声明 go 1.21,代码调 slices.Repeat):
$ go vet ./...
main.go:9:21: slices.Repeat requires go1.23 or later (module is go1.21)
exit status 1更狠的是它在 go test 阶段就会拦你:同一 module 直接 go test,输出 FAIL p [build failed] 并附带上述诊断;go test -vet=off 才放行。升级工具链后老项目 CI 突然红一片,多半就是这个检查器干的——正确解法是修 go.mod 版本声明或换低版本可用的 API,而不是 -vet=off。
8.2 相关但更早的检查器
lostcancel(context 包的 WithCancel 返回的 cancel 被丢弃/未在所有路径调用)早已是默认检查,测 context 泄漏时顺带会被它兜底;其余 printf 类、tests 类分析器一句话带过:go test 默认就会跑 vet 子集,写测试时不用另起命令。
9. 相邻包指针
- testing/fstest:
fstest.MapFS用内存 map 构造fs.FS,fstest.TestFS(fsys, expected...)校验任何 FS 实现的契约。适合测 io/fs 消费方,本手册不展开。 - net/http/httptest:
httptest.NewServer/NewRecorder测 HTTP handler,完整参考见 net-http.md。Go 1.27 新增httptest.NewTestServer(testing.TB, http.Handler)(api/go1.27.txt#76608),把 server 生命周期挂到t.Cleanup。
10. 陷阱汇总
| 陷阱 | 后果与正解(出处) |
|---|---|
在子 goroutine 里调 t.Fatal |
FailNow 只能停当前 goroutine;go doc 原文 “must be called from the goroutine running the test … does not stop those other goroutines”。子 goroutine 用 t.Error + 通知主 goroutine |
t.Parallel() 后调 t.Chdir/t.Setenv |
实测 panic “testing: test using t.Setenv, t.Chdir, or cryptotest.SetGlobalRandom can not use t.Parallel”,整个测试二进制 exit 2 |
测试里 os.Exit(0) 想"提前通关" |
实测 panic “unexpected call to os.Exit(0) during test”(-test.paniconexit0 默认开);收尾一律交给 TestMain 的 os.Exit(m.Run()) |
TestMain 只 return 不 os.Exit(m.Run()) |
测试失败被吞;os.Exit 也不跑 defer(go doc os.Exit) |
| 循环里并行子测试捕获共享变量 | Go 1.22 前每轮共享同一循环变量,1.22+ 每轮独立;低版本兼容写法仍建议显式拷贝 |
b.Loop() 循环里 break/return 提前退出 |
无条件退出实测 FAIL(“benchmark function returned without B.Loop() == false”);带条件退出可能 PASS 但量到的只是循环开销,数据失真 |
b.Loop() 与 for i:=0;i<b.N;i++ 混用 |
go doc 原文 “should either use Loop or contain a loop to b.N, but not both” |
只写 t.Helper() 之前有逻辑 |
错误行号指向辅助函数内部;Helper 必须是第一行 |
以为存在 -subtests flag |
实测 1.27 报 “flag provided but not defined”;子测试用 -run 父/子 正则控制 |
-count=0 当"快速冒烟" |
实测不跑任何测试(warning: no tests to run)仍 PASS,给假绿 |
基准只跑一次 -count=1 就下结论 |
实测同基准相邻两次 23→47 ns/op;波动大就 -count=5+ 并用 benchstat |
| 模糊语料目录不提交 git | testdata/fuzz/FuzzXxx 是回归种子,丢了 CI 就失去历史反例 |
在 CI 里常驻 -fuzz |
go test 默认只跑种子(go doc testing.F);变异是本地探索,CI 塞 fuzz 会吃掉 runner |
| 气泡内用真实网络/外部进程 | 阻塞在 IO 的 goroutine 不 durably blocked,气泡永不空闲,测试挂到超时(package doc 纪律) |
| 并行测试访问共享全局状态 | go doc testing.T.Parallel:并行只与并行互跑;全局状态用 t.Setenv/t.TempDir 隔离,但注意它们又与 Parallel 互斥——二选一 |
实战建议
- 新写基准一律
for b.Loop():setup 计时、StopTimer 配对、循环体保活全由框架兜底,比手工b.ResetTimer()少一类错。老基准迁移时删掉ResetTimer/循环上界即可。 - 测超时、重试、心跳这类时间敏感逻辑,优先 synctest:虚拟时钟让 5s 超时用 0s 测完,且结果确定;配
-race仍有必要(synctest 管时序,race 管数据竞争,两者正交)。 - 用
t.Context()作为 goroutine 型测试的统一取消信号,配合t.Cleanup收口;别再手写 WithCancel + defer cancel 的样板。 - 每个解析/编码/协议入口配一个 FuzzXxx,种子语料进 git,CI 只跑种子;发版前本地
-fuzz各目标 10~30 分钟,发现的反例一律落 testdata 固化成回归。 - TestMain 只用于真正包级共享的资源(起容器、连测试库);单个测试的资源用
t.Cleanup/t.TempDir,并行安全且互不污染。 - 升 go 版本后先跑一遍
go test ./...看 stdversion 是否报警——它专抓"go.mod 版本声明与所用 API 不符"的长期慢性病,比等到用户编译失败便宜得多。 - flaky 排查三板斧:
-shuffle=on(顺序依赖)→-count=10(重复触发)→ 记下输出的 shuffle seed 用-shuffle=<seed>复现。