Go基准测试必须用go test -bench启动,手动运行无效;函数需为func BenchmarkXxx(*testing.B)格式;b.ResetTimer()应在初始化后、循环前调用,避免准备时间计入耗时。

Go benchmark test 必须用 go test -bench 启动
直接运行 go run 或手动调用 BenchmarkXXX 函数不会触发基准测试框架的计时、迭代和结果统计逻辑,测出的数字毫无意义。Go 的 testing.B 对象在 -bench 模式下才自动控制循环次数、预热、采样和内存分配统计。
常见错误包括:
- 在普通
main函数里调用time.Now()手动测耗时 —— 忽略 GC 干扰、CPU 频率波动、编译器优化干扰 - 忘记加
-benchmem,导致看不到Allocs/op和B/op,漏掉内存分配瓶颈 - 用
-bench=.匹配所有函数,但没加-run=^$排除单元测试,导致 benchmark 被中断或污染
正确启动方式示例:
go test -bench=BenchmarkParseJSON -benchmem -run=^$
Benchmark 函数签名不能省略 *testing.B 参数
Go 要求基准函数必须是 func BenchmarkXxx(*testing.B) 形式,且函数名以 Benchmark 开头、首字母大写。否则 go test -bench 会直接忽略。
立即学习“go语言免费学习笔记(深入)”;
容易被忽略的关键点:
-
b.ResetTimer()应在初始化代码(如构造输入数据)之后、实际压测循环之前调用,否则把准备时间也算进耗时 -
b.ReportAllocs()可显式开启内存统计(-benchmem已默认启用,但某些 CI 环境可能关闭) - 不要在循环内调用
b.StopTimer()/b.StartTimer()来“排除某段代码”——这会让迭代次数失真;应把非目标逻辑移到循环外
典型结构示例:
func BenchmarkParseJSON(b *testing.B) {
data := loadSampleJSON() // 预加载,不计入耗时
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = json.Unmarshal(data, &struct{}{})
}
}
避免编译器优化导致 Benchmark 结果为 0 ns/op
如果被测函数返回值未被使用,且 Go 编译器判定该调用无副作用,整个调用可能被完全优化掉,最终显示 0 ns/op —— 这不是性能好,是没跑。
解决方法只有两个:
- 用
blackhole方式强制保留结果:将返回值赋给全局变量var result = ...,或调用runtime.KeepAlive() - 更推荐的是:用
testify/assert或原生if做轻量断言(例如if len(out) == 0 { b.Fatal("empty") }),既防优化又验证逻辑正确性
反模式示例(会被优化):
for i := 0; i < b.N; i++ {
json.Unmarshal(data, &v) // v 未被读取,整行可能消失
}
修复后:
var blackhole interface{}
for i := 0; i < b.N; i++ {
err := json.Unmarshal(data, &v)
if err != nil {
b.Fatal(err)
}
blackhole = v // 强制保留 v 的使用
}
对比多个实现时,用 -benchmem + -count=5 看稳定性
单次 benchmark 结果波动大,尤其涉及内存分配或系统调用时。仅看一次 ns/op 容易误判。真正有参考价值的是多次运行后的中位数与标准差。
建议组合参数:
-
-benchmem:固定开启内存指标,避免遗漏Allocs/op高但ns/op低的“伪快”实现 -
-count=5:运行 5 轮,go test会自动输出每轮结果及汇总统计 -
-benchtime=5s:延长单轮运行时间(默认 1s),减少启动/预热噪声占比
注意:不同实现的 b.N 可能不同(benchmark 框架自动调整迭代次数以满足 -benchtime),所以必须比 ns/op,而不是总耗时或 b.N 值。
真实瓶颈常藏在 Allocs/op 里,比如一个函数快 20%,但 Allocs/op 高 10 倍 —— 在高频服务中反而更容易触发 GC 停顿。别只盯着 ns/op 看。











