go test -bench=. 仅识别满足三条件的函数:名以Benchmark开头且驼峰大写、参数为*testing.B、文件名以_test.go结尾;b.N为动态迭代次数,须用for i := 0; i < b.N; i++;初始化需b.ResetTimer()隔离,结果须被消费以防编译器优化。

函数名和签名不合法,go test -bench=. 就会静默跳过
不是所有叫 Benchmark 开头的函数都能被识别——必须同时满足三个硬性条件:函数名是 BenchmarkXxx(首字母大写、驼峰)、参数类型是 *testing.B(带星号指针)、文件名以 _test.go 结尾。漏掉任意一个,go test -bench=. 既不报错也不提示,直接当没这回事。
常见错误现象:no tests to run 或输出里压根没你的函数名;或者显示 benchmarked 0 times。
-
BenchmarkFibonacci✅ 合法;benchmark_fib、TestBenchmarkFib、Benchmarkfib❌ 全部无效 - 签名必须是
func BenchmarkXxx(b *testing.B);写成func BenchmarkXxx(b testing.B)(漏指针)或func BenchmarkXxx(b *testing.T)(类型错)都会失效 - 别把基准测试写在
main.go或普通.go文件里——必须放在同包的xxx_test.go中
b.N 是动态值,不能手写固定循环次数
b.N 不是你能控制的常量,而是 Go 测试框架根据实际耗时自动调整的迭代次数。它的目标是让整个基准函数运行时间接近默认的 1 秒(可用 -benchtime=3s 调整)。你手动写 for i := 0; i < 1000000 不仅让 b.N 失效,还会导致结果不可比、CI 环境下波动剧烈。
- 正确写法永远是
for i := 0; i < b.N; i++,把待测逻辑放进去 - 如果函数极快(比如纳秒级),
b.N可能飙升到千万甚至上亿——这是正常行为,说明框架在努力稳定测量 - 不要用
if b.N == 1做一次性初始化——每次运行b.N都可能不同,逻辑会错乱
初始化开销必须用 b.ResetTimer() 剥离,否则 ns/op 完全失真
像 make(map[int]int, 1e6)、json.Marshal 预热数据、打开文件这类操作,如果混在计时区内,就会把 setup 时间摊到每次操作上,ns/op 变成“初始化 + 执行”的混合值,尤其当初始化远重于主逻辑时,结果毫无参考意义。
立即学习“go语言免费学习笔记(深入)”;
- 标准结构:setup 代码 →
b.ResetTimer()→for i := 0; i < b.N; i++{ 主逻辑 } - 如果主逻辑有副作用(比如修改全局 map),每次迭代前需重置状态,也得放在
b.StopTimer()区块内,再b.StartTimer() -
b.ReportAllocs()要在b.ResetTimer()之后调用,否则内存统计包含 setup 阶段
不强制使用返回值,编译器可能直接优化掉整个调用
Go 编译器对无副作用代码极其激进:如果你测的是 fib(40) 却没接返回值,或者接了又没用,它可能连函数体都不生成——汇编里根本找不到对应逻辑,ns/op 接近 0 不代表快,是压根没跑。
- 最简方案:加一行
blackhole := yourFunc(),再加blackhole = blackhole(防后续优化) - 更稳妥:用
runtime.KeepAlive(blackhole)或赋值给包级变量(注意别引入锁或 GC 干扰) - 验证是否被优化:加
go test -gcflags="-l" -bench=.关闭内联,看结果是否突变;但日常不用总关,重点是确保结果被消费
BenchmarkXxx,而是意识到每行代码都在影响计时边界、逃逸分析和编译器判断——哪怕多一个变量声明、少一次 ResetTimer(),都可能让 ns/op 偏差几倍。



















