Go基准测试需用b.Run手动循环驱动表格用例,子测试名须唯一可读(如fmt.Sprintf("N%d", tc.n)),循环内需tc := tc避免闭包捕获,每次b.Run前调用b.ResetTimer()或b.ReportAllocs()。

怎么写表格驱动的 Benchmark 函数
Go 的基准测试不支持像 Test 那样直接用结构体切片驱动,必须手动循环调用 b.Run。核心是:每个子基准测试名要唯一、可读,且不能在循环里直接用变量插值(会闭包捕获同一值)。
常见错误现象:BenchmarkFoo-8 0 0 ns/op,跑完显示 0 次,说明没真正执行;或者所有子项名字都叫 "case",无法区分。
- 子测试名建议用
b.Run(fmt.Sprintf("With%dItems", tc.n), ...),别写死字符串 - 闭包陷阱:用
tc := tc在循环内重新声明局部变量,避免所有b.Run引用同一个地址 - 每次
b.Run内部必须调用b.ReportAllocs()或b.ResetTimer()(如果前置有初始化开销)
func BenchmarkSort(t *testing.B) {
cases := []struct{ n int }{{100}, {1000}, {10000}}
for _, tc := range cases {
tc := tc // 防止闭包捕获
t.Run(fmt.Sprintf("N%d", tc.n), func(b *testing.B) {
data := make([]int, tc.n)
b.ResetTimer()
for i := 0; i < b.N; i++ {
sort.Ints(data)
}
})
}
}
Benchmark 里为什么不能用 fmt.Println 或全局变量
基准测试运行时,b.N 是框架自动调整的迭代次数,目标是让单次耗时稳定在 100ms–1s 左右。任何非被测逻辑的 I/O 或状态污染都会扭曲结果,甚至导致 panic。
使用场景:你只想测函数本身的吞吐和分配,不是测日志性能或并发安全。
立即学习“go语言免费学习笔记(深入)”;
-
fmt.Println、log.Print会触发锁和系统调用,大幅拉低ns/op数值,且结果不可复现 - 修改全局变量(比如计数器、缓存 map)会导致多次
b.N迭代间状态污染,尤其在并行b.RunParallel下直接崩溃 - 如果真需要调试,用
b.Log(...),它只在加-test.v时输出,不影响计时
什么时候该用 b.RunParallel 而不是普通循环
b.RunParallel 不是用来“加速基准测试”的,而是用来模拟多 goroutine 并发调用被测函数的真实负载,比如 HTTP handler、连接池获取、map 并发读写等场景。
性能影响明显:普通循环是串行压测,b.RunParallel 会启动多个 goroutine 同时调用函数,暴露锁竞争、GC 压力、cache line false sharing 等问题。
- 仅当被测函数本身设计为并发安全,且你想验证其并发性能时才用
- 不要在
b.RunParallel里做初始化(如make切片),它不保证执行顺序,也不重置 timer - 子 goroutine 数量由
GOMAXPROCS和框架动态决定,不能硬编码;想控并发度,改GOMAXPROCS或用外部限流
func BenchmarkConcurrentMapRead(b *testing.B) {
m := sync.Map{}
for i := 0; i < 1000; i++ {
m.Store(i, i)
}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
m.Load(123)
}
})
}
为什么 go test -bench 结果里 allocs/op 有时不准
allocs/op 是基于 runtime.ReadMemStats 统计的堆分配次数,但它只捕获「显式堆分配」,比如 make、new、切片扩容、逃逸到堆的变量。编译器优化(如栈上分配)会让这个数字偏低,甚至为 0,不代表没分配。
兼容性影响:Go 1.21+ 对小对象分配做了更多栈上优化,同一段代码在不同版本下 allocs/op 可能差几倍,但 ns/op 更稳定。
- 别单看
allocs/op判断内存优劣,结合benchstat看趋势,或用go tool pprof看实际 heap profile - 如果函数里有
append且底层数组频繁扩容,allocs/op会飙升,这时预分配容量比纠结数字更有用 -
-benchmem必须显式加上,否则不统计分配,结果里不会显示allocs/op
b.N 是动态调整的,但人写的初始化逻辑常会误把它当固定次数来用——比如在循环外预生成数据却忘了按 b.N 规模准备,结果测的其实是 cache 命中率而非函数本身。











