Go benchmark函数名必须以Benchmark开头、接收*testing.B参数且首字母大写,如BenchmarkMapAccess;需调用b.ResetTimer()重置计时器,并用b.N控制循环次数。

Go benchmark 函数名必须以 Benchmark 开头且接收 *testing.B
Go 的 go test -bench 只会执行签名匹配 func BenchmarkXxx(*testing.B) 的函数。名字写成 TestBenchmarkXxx 或参数用 testing.T 都不会被识别,也不会报错,只会静默跳过。
- 函数必须是可导出的(首字母大写),比如
BenchmarkMapAccess✅,benchmarkMapAccess❌ - 不能漏掉
b.ResetTimer()—— 如果前置初始化耗时较长(如构建百万级 map),不重置会导致这部分时间被计入基准结果 - 必须调用
b.N控制循环次数:直接写for i := 0; i 是错的;正确写法是for i := 0; i
func BenchmarkMapLookup(b *testing.B) {
m := make(map[string]int)
for i := 0; i < 1e5; i++ {
m[fmt.Sprintf("key-%d", i)] = i
}
b.ResetTimer() // 关键:排除初始化开销
for i := 0; i < b.N; i++ {
_ = m["key-12345"]
}
}
b.ReportAllocs() 和 -benchmem 要一起用才有效
只在函数里调用 b.ReportAllocs() 不会自动打印内存分配统计;必须配合命令行参数 -benchmem,否则输出中看不到 B/op 和 allocs/op。
-
go test -bench=. -benchmem是标准组合,缺一不可 - 若函数内没调
b.ReportAllocs(),即使加了-benchmem,Go 也会跳过内存采样(尤其在老版本 Go 中更明显) - 注意:字符串拼接、
fmt.Sprintf、切片扩容等极易触发隐式分配,别只看 CPU 时间
避免在 benchmark 中使用 time.Sleep 或阻塞 I/O
基准测试要求测量纯计算路径的性能,任何外部延迟都会污染结果。用 time.Sleep(100 * time.Millisecond) 不仅让 b.N 失效(因为每次迭代时间远超纳秒级),还会导致 go test 自动延长采样轮次,最终输出的 ns/op 完全不可比。
- 网络请求、文件读写、channel 等待、锁竞争(除非你就是在测锁)都应 mock 或预热后绕过
- 如果真要测 I/O 类操作,应改用
testing.B.Run分组 + 显式记录time.Since,并禁用b.N循环(即设b.N = 1),再手动重复多次取平均——但这已不属于标准 benchmark 范畴 - 常见误写:
http.Get直接进 loop,结果所有时间都花在 DNS 解析和 TCP 握手上
注意 b.N 的自适应机制与冷热启动偏差
Go 的 benchmark 不是固定运行 b.N 次,而是先试探性跑少量次数估算单次耗时,再动态调整 b.N 到约 1 秒总时长。这意味着:
立即学习“go语言免费学习笔记(深入)”;
- 首次运行可能因 CPU 频率未拉满、cache 未命中、GC 未触发而偏快;建议用
-count=5多轮取平均 - 带指针或堆对象的代码可能在第 2–3 轮触发 GC,后续
ns/op突然升高,此时需结合-gcflags="-m"看逃逸分析 - 不要在单个
Benchmark函数里测多个算法并用if b.N%2==0切换逻辑——这会让 Go 无法稳定估算单次耗时,b.N波动剧烈,结果失真
最常被忽略的一点:benchmark 文件名必须是 _test.go,且不能和普通 test 混在同一函数里;一旦在 Benchmark 函数中调用了未导出的 helper 函数,又没在 _test.go 里定义它,编译就失败——但错误提示只显示 “undefined”,不会告诉你缺的是测试专用文件。











