正确做法是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 的优势。
立即学习“go语言免费学习笔记(深入)”;
注意:不要在循环里反复调 time.Now() 测单次调用——系统调用开销本身会影响结果;应测足够多次后取总耗时再除以次数。
- 适合快速验证、日志打点、开发期粗略观察
- 不适合性能对比或压测结论,尤其对 sub-microsecond 级操作误差明显
- 避免用
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();(无返回、无副作用)——大概率被删光
复杂逻辑的耗时统计,真正难的从来不是怎么记时间,而是确保你测的确实是你要测的那部分代码,且它没被编译器悄悄绕过。










