Benchmark函数必须以Benchmark开头、参数为*testing.B、置于同包的_test.go文件中,否则go test -bench=.会静默跳过;B.N是动态循环次数,不可用log或fmt输出。

怎么写一个能跑通的 benchmark 函数
Go 的 testing.B 不是随便起个函数名就能被 go test -bench 识别的,必须严格满足命名和签名规则。否则 go test -bench=. 会直接跳过,连报错都没有,容易误以为“没跑”。
- 函数名必须以
Benchmark开头,后面接驼峰名(如BenchmarkMapVsSlice) - 参数必须是
*testing.B,不能少星号、不能换类型、不能多参数 - 必须放在
_test.go文件里,且包名和待测代码一致 - 别在函数里用
log或fmt.Println——B.N是循环次数,不是固定值,输出会刷屏还干扰计时
示例:
func BenchmarkStringConcat(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = "hello" + "world"
}
}
对比两种实现时,怎么避免干扰项
benchmark 的核心是「只让被测逻辑差异生效」,但实际常因初始化、缓存、GC 干扰结果。比如测试 map[string]int 和 sync.Map,如果每次循环都新建 map,开销就全算到被测逻辑头上,失真。
- 把初始化(如构造数据、预热结构)放到
b.ResetTimer()之前,确保不计入耗时 - 用
b.ReportAllocs()看内存分配,避免一种方案因频繁 alloc 拖慢,却误判为“逻辑慢” - 加
b.RunParallel测试并发场景时,注意变量捕获:闭包里别直接用循环变量,要用局部拷贝 - 运行时加
-benchmem -count=5多次采样,看方差——如果每次结果波动超 10%,说明有外部干扰(如 GC 正好卡在某次循环)
为什么 go test -bench=. 跑出来的时间不准
默认输出的 ns/op 是单次操作平均纳秒数,但它隐含两个前提:被测逻辑可稳定重复、无副作用。而真实代码常有状态残留或依赖外部时序(比如首次调用触发 lazy init),导致前几次极慢、后面变快。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- Go benchmark 默认会自动调整
B.N让总耗时接近 1 秒,所以小函数可能跑几百万次,大函数只跑几次——这本身没问题,但你要确认B.N是否足够覆盖 warmup 阶段 - 遇到首轮明显偏慢,手动在循环前加一次「预热调用」,再调
b.ResetTimer() - 不要只看
ns/op,重点关注BenchmarkXXX-8 1000000 1234 ns/op中的-8—— 它代表 GOMAXPROCS 值,跨机器对比前得统一设置GOMAXPROCS=1,否则并发调度差异会掩盖真实性能
怎么判断两种方案真的有性能差别
benchmark 数值差 5% 未必有意义。Go 自身调度、CPU 频率波动、TLB miss 都可能导致 ±3% 波动。盲目优化微小差异,反而增加维护成本。
立即学习“go语言免费学习笔记(深入)”;
- 用
benchstat(go install golang.org/x/perf/cmd/benchstat@latest)比对两组结果,它会做统计检验,输出类似geomean: -12.34% (p=0.002)—— p 值小于 0.05 才算显著 - 关注
allocs/op和B/op:内存分配少往往意味着 GC 压力小,长期运行比单纯ns/op更关键 - 如果两种方案在不同数据规模下优劣反转(比如小 slice 用
append快,大 slice 用预分配快),那就得结合真实业务数据量来测,而不是只测一个点
真正难的从来不是跑出数字,而是确认那个「被测逻辑」是否干净、那个「业务场景」是否被覆盖到了。漏掉一次预热、混进一行日志、或者忘了锁住 GOMAXPROCS,结果就可能完全误导你。


















