正确做法是setup完毕后先调b.StopTimer()暂停计时,再调b.ResetTimer()清零并重启;二者顺序不可颠倒,否则setup时间仍被计入。
testing.B 的 ResetTimer() 和 StopTimer() 控制计时边界Go 的基准测试默认从 BenchmarkXxx 函数入口开始计时,但实际只想测核心逻辑——比如初始化开销(建 map、读配置)不该计入。这时候必须手动干预计时器。
常见错误是直接在函数开头调 b.ResetTimer(),结果把 setup 阶段也统计进去了;正确做法是:setup 完毕后调 b.StopTimer()(停),再调 b.ResetTimer()(清零并重启),之后的循环才真正被计时。
b.StopTimer():暂停计时,不重置已记录时间b.ResetTimer():清空当前耗时并重新开始计时Stop 再 Reset,否则 Reset 会立即生效,setup 时间仍被计入func BenchmarkMapAccess(b *testing.B) {
// setup:不计入耗时
m := make(map[int]int)
for i := 0; i < 1000; i++ {
m[i] = i * 2
}
b.StopTimer() // 停掉 setup 阶段计时
b.ResetTimer() // 清零,准备测核心逻辑
for i := 0; i < b.N; i++ {
_ = m[i%1000] // 真正要测的操作
}}
time.Now() 手动计时只适用于非基准场景
如果你不在写 go test -bench,只是想临时看某个函数执行多久(比如调试 HTTP handler 或 CLI 命令),time.Now() + time.Since() 最直接。但它无法自动处理 GC、调度抖动、多次运行取平均等 benchmark 的优势。
注意:不要在循环里反复调 time.Now() 测单次调用——系统调用开销本身会影响结果;应测足够多次后取总耗时再除以次数。
fmt.Printf 混在计时块里——I/O 会严重污染结果func main() {
start := time.Now()
result := heavyComputation()
elapsed := time.Since(start)
fmt.Printf("heavyComputation took %v\n", elapsed) // 仅用于观察,勿用于 benchmark 报告
}b.N 的自适应机制和 -benchmem 内存统计Go 的 testing.B 不是固定跑 100 次,而是根据预设时间(默认 1 秒)动态调整 b.N:让测试尽可能接近这个目标时长。这意味着你看到的 ns/op 是“单次操作平均耗时”,不是某一次的瞬时值。
如果函数还分配内存,漏掉 -benchmem 就会错过关键指标。比如看似很快的函数,可能每 op 分配 1KB 临时 slice,频繁 GC 后整体吞吐暴跌。
-benchmem: go test -bench=. -benchmem
B/op 表示每次操作分配字节数,allocs/op 是分配次数B/op 非零,优先看能否复用对象(如 sync.Pool、预分配 slice)Go 编译器很激进。如果你写的 benchmark 函数里,结果没被使用、输入是常量、逻辑能被完全推导,编译器可能直接优化掉整段代码——最终测到的是“0 ns/op”,毫无意义。
典型表现:函数返回值未被消费,或者中间变量全被优化。解决方法是用 blackhole 抑制优化(Go 1.21+ 推荐用 testing.B.ReportMetric 配合显式赋值,但最简单仍是 blackhole)。
var result T,或传给 runtime.KeepAlive()
result := yourFunc() + b.ReportMetric(float64(result), "result")(Go 1.21+)yourFunc();(无返回、无副作用)——大概率被删光复杂逻辑的耗时统计,真正难的从来不是怎么记时间,而是确保你测的确实是你要测的那部分代码,且它没被编译器悄悄绕过。