基准测试函数签名必须是func BenchmarkXxx(*testing.B),且需在_test.go文件中;初始化代码前调用b.ResetTimer(),主循环中用b.N控制迭代。

基准测试函数签名必须是 func BenchmarkXxx(*testing.B)
Go 的基准测试不是随便起个函数名就能跑的——必须以 Benchmark 开头,且唯一参数类型是 *testing.B,返回值不能有。写成 func BenchmarkAdd(b *testing.B) 才会被 go test -bench 识别;写成 func BenchmarkAdd(b testing.B)(少指针)或 func BenchmarkAdd(b *testing.B) error(多返回值)都会被跳过,还不报错,容易误以为“没运行”。
常见错误现象:go test -bench=. 输出空白,或只显示 ok . 0.001s,实际一个 benchmark 没跑。
- 函数必须在
_test.go文件里,且文件名匹配(如math_test.go) - 不能和普通测试函数混在同一个函数名下(比如同时叫
TestAdd和BenchmarkAdd是允许的,但别写成TestAdd里调用b.N) - 如果包里没有其他测试,
go test -bench=.默认不运行——得加-run=^$或先确保有任意一个TestXxx存在
b.ResetTimer() 和 b.StopTimer() 的使用时机
基准测试默认从函数入口开始计时,但初始化、预热、建表等非核心逻辑不该计入耗时。比如测试 map 查找前要先建好百万条数据,这部分必须排除。
正确做法:用 b.StopTimer() 暂停计时 → 做准备 → b.StartTimer() 恢复;若准备只做一次,更推荐 b.ResetTimer()(它会清零已计时并重启,且隐含一次 StartTimer())。
-
b.ResetTimer()应该放在准备代码之后、主循环之前,且只能调用一次 -
b.StopTimer()和b.StartTimer()可多次配对,适合分段测量(例如单独看序列化 vs 网络传输) - 漏掉
ResetTimer()导致初始化时间被计入,结果偏高;放错位置(比如放在循环里)会导致计时器反复启停,b.N迭代次数异常
func BenchmarkMapLookup(b *testing.B) {
m := make(map[int]int)
for i := 0; i < 1e6; i++ {
m[i] = i * 2
}
b.ResetTimer() // ✅ 关键:重置后才开始测查找
for i := 0; i < b.N; i++ {
_ = m[i%1e6]
}
}
b.N 不是固定次数,而是由 Go 自动调整的迭代数
b.N 是 runtime 根据目标最小耗时(默认 1 秒)动态决定的。第一次试跑可能只执行 100 次,发现总耗时太短,就指数增长到 1000、10000……直到单轮稳定在几百毫秒以上。所以不能假设 b.N == 1000,也不能在循环外依赖它的值做初始化(比如 make([]int, b.N) 是错的,因为 b.N 在每次自适应过程中会变)。
- 所有依赖
b.N的分配、构造必须放在b.ResetTimer()之后,或明确用固定尺寸(如make([]int, 1e6)) - 想控制最小运行时间,用
-benchmem -benchtime=3s;想压测吞吐,可加-count=5跑多次取平均 - 如果函数太快(纳秒级),
b.N可能高达千万,注意别触发 GC 或内存溢出
避免逃逸和内存分配干扰真实性能
基准测试中一次 fmt.Sprintf、append 未预估容量、甚至闭包捕获变量,都可能引发堆分配,让 -benchmem 显示高 B/op,掩盖算法本身开销。这不是 bug,但会误导优化方向。
- 用
go test -bench=. -benchmem看每操作分配字节数和次数;理想是0 B/op和0 allocs/op - 字符串拼接优先用
strings.Builder,slice 操作提前make(..., 0, n) - 避免在循环里创建新结构体指针或调用可能逃逸的函数(如
log.Printf) - 不确定是否逃逸?加
-gcflags="-m"看编译器提示,或用pprof对比 allocs
最常被忽略的是:基准测试里用了 fmt.Println 调试,忘记删掉——它不仅分配内存,还会锁 stdout,直接让结果失真几个数量级。

















