基准测试函数必须满足四个条件:文件名以_test.go结尾且与被测代码同包;函数名以Benchmark开头后接大驼峰标识符;参数为*testing.B类型;函数体内必须包含b.N次循环。

Go 函数的基准测试不能靠 time.Now() 手动计时,也不能写个固定次数的循环——那样测出来的 ns/op 完全不可比、不可复现,甚至被编译器优化掉逻辑。
func BenchmarkXxx(b *testing.B) 必须严格满足这四个条件
漏掉任意一个,go test -bench=. 就会静默跳过,输出里压根不出现你的函数:
- 文件名必须是
xxx_test.go,且和被测代码在同一个包(比如被测函数在utils.go,基准测试就得放在同目录的utils_test.go) - 函数名必须以
Benchmark开头,后接大驼峰标识符(BenchmarkJSONMarshal✅,benchmarkJSONMarshal❌,BenchJSONMarshal❌) - 参数签名必须是
func(b *testing.B)(*不能少,类型不能写成testing.B或interface{}) - 函数体必须包含
for i := 0; i 循环,且被测逻辑在循环内
初始化开销必须用 b.ResetTimer() 隔离
如果你在循环外做了 data := make([]byte, 1e6) 或 json.Unmarshal 预热,又没调 b.ResetTimer(),那这部分耗时和内存分配就全算进 ns/op 和 B/op 了——结果反映的是“初始化 + 执行”,不是你真正想测的逻辑。
标准结构是:
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 先做所有 setup(构造数据、打开文件、预热缓存等)
- 立刻调用
b.ResetTimer() - 再开始
for i := 0; i 循环执行主逻辑 -
b.ReportAllocs()要放在b.ResetTimer()之后,否则内存统计含 setup 阶段
怎么对比两个函数的性能差异才靠谱
手动跑两次、看一眼 ns/op 差多少,很容易被系统噪声带偏。正确做法是:
- 用
go test -bench=^BenchmarkFoo$ -benchmem -count=5 | tee bench-old.txt存基线 - 改完代码后,同样命令存为
bench-new.txt - 运行
benchstat bench-old.txt bench-new.txt(需先go install golang.org/x/perf/cmd/benchstat@latest)
benchstat 不是比较平均值,而是用 Welch’s t-test 判断差异是否统计显著,并给出置信区间。它能告诉你:“新实现快了 12%,p < 0.01”——而不是“好像快了一点”。
常见踩坑点:b.N 不是你可以控制的变量
b.N 是框架动态决定的整数,目标是让整个循环总耗时接近 -benchtime(默认 1 秒)。它可能从 1 跳到 1000 万,这是正常行为。你如果写 if b.N == 1 { init() },逻辑必然错乱;如果手动写死 for i := 0; i ,等于绕过整个基准机制,<code>ns/op 失去意义。
真正容易被忽略的是:被测逻辑的结果必须被消费(比如赋给 _ 或全局变量),否则编译器可能直接优化掉整段计算——你测的根本不是你想测的代码。

















