Go基准测试函数必须以Benchmark为前缀、参数为*testing.B且文件名以_test.go结尾;运行命令为go test -bench=.;b.N由框架动态调整以确保总耗时约1秒,不可手动赋值。

Go基准测试必须用Benchmark前缀函数
Go的go test只识别以Benchmark开头、参数为*testing.B的函数。写成TestXxx或benchXxx都不会被运行。
- 函数签名必须是
func BenchmarkXxx(b *testing.B),大小写和参数类型都不能错 - 文件名需以
_test.go结尾,且与被测代码在同一包下(通常同目录) - 运行命令是
go test -bench=.,不是go run或go build
b.N不是循环次数,而是框架动态调整的迭代基数
Go基准测试会自动多次运行函数,并根据耗时调整b.N值,目标是让单次执行时间稳定在约1秒左右。你不能手动设b.N = 1000来“控制次数”——这会被忽略,且破坏结果准确性。
- 所有逻辑必须放在
for i := 0; i 循环内,否则计时不包含实际工作 - 初始化开销(如构建大slice、打开文件)应放在
b.ResetTimer()之前,避免计入基准耗时 - 若需预热(warm-up),可加一次空跑:
for i := 0; i ,再调b.ResetTimer()
func BenchmarkMapAccess(b *testing.B) {
m := make(map[int]int)
for i := 0; i < 1000; i++ {
m[i] = i * 2
}
b.ResetTimer() // 重置计时器,跳过建map的开销
for i := 0; i < b.N; i++ {
_ = m[i%1000] // 实际被测操作
}
}避免在基准测试中触发GC或内存分配干扰结果
频繁分配内存会引入GC抖动,导致ns/op波动大、不可比。尤其注意字符串拼接、fmt.Sprintf、切片append未预分配等隐式分配。
- 用
b.ReportAllocs()开启内存统计,观察B/op和allocs/op - 提前分配好切片容量:
result := make([]int, 0, b.N),而非make([]int, b.N)(后者可能浪费空间) - 避免在循环内调用
log.Print、fmt.Println——它们分配并写IO,直接让基准失效
对比多个实现时,用子基准测试保持环境一致
不要写多个独立的BenchmarkXxx函数来比性能——它们可能在不同GC周期、不同CPU调度下运行。改用b.Run做子基准,共享同一轮驱动逻辑。
- 每个
b.Run子项单独计时,但复用外层预热和初始化 - 名字用短标识符(如
"map"、"slice"),避免空格或特殊字符 - 子基准里仍要遵守
b.N循环规则,不能省略
func BenchmarkSearch(b *testing.B) {
data := make([]int, 1e6)
for i := range data {
data[i] = i
}
b.Run("map", func(b *testing.B) {
m := make(map[int]bool)
for _, x := range data {
m[x] = true
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = m[data[i%len(data)]]
}
})
b.Run("slice", func(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
found := false
for _, x := range data {
if x == data[i%len(data)] {
found = true
break
}
}
_ = found
}
})
}真实压测中,b.N可能从几万跳到几百万,取决于函数快慢;如果发现allocs/op远高于预期,大概率是某处悄悄new了对象——这时候go tool pprof比盯着数字更有用。










