go基准测试函数必须命名为benchmarkxxx且签名func benchmarkxxx(b *testing.b),置于同包_test.go文件中;需用b.stoptimer/starttimer控制计时,避免循环内重复操作或内存逃逸,并用pprof预筛热点。

怎么写一个能跑起来的 Benchmark 函数
Go 的基准测试函数不是随便起个名就能被 go test -bench 识别的,必须严格符合命名和签名规范。写错就等于没写。
- 函数名必须以
Benchmark开头,后面接大驼峰标识符,比如BenchmarkMapInsert,不能是benchmarkMapInsert或BenchMapInsert - 函数签名必须是
func BenchmarkXxx(b *testing.B),参数类型不能错,*testing.B少个星号或写成testing.B都会编译失败 - 必须放在
_test.go文件里,且包名和待测代码一致(不能是main或其他独立包) - 别在
Benchmark函数里直接用fmt.Println打印——输出会被吞掉,要用b.Log或b.ReportAllocs看内存分配
testing.B 的 ResetTimer 和 StopTimer 什么时候该用
基准测试默认从函数入口开始计时,但初始化、预热、构造数据这些不该计入耗时。不处理,测出来的数字完全失真。
-
b.StopTimer():暂停计时器,适合放在循环前做 setup(如初始化 map、生成测试数据) -
b.StartTimer():恢复计时,通常紧跟在 setup 完成后 -
b.ResetTimer():重置已统计的耗时和迭代次数,常用于丢弃冷启动抖动(比如第一次 GC 影响),建议在 warmup 后、正式循环前调用一次 - 错误用法:在
b.N循环内部反复Stop/Start,这会让b.N失效,实际执行次数远少于预期
为什么 go test -bench=. 什么都没输出
不是代码错了,大概率是没触发基准测试的运行条件——Go 默认跳过所有 Benchmark,除非显式启用且匹配到函数。
- 必须加
-bench参数,哪怕只是-bench=^$(匹配空字符串)也会被忽略,得用-bench=.或具体函数名如-bench=BenchmarkJSON - 如果包里没有
go:build标签限制,但文件名不含_test.go,go test根本不会加载它 - 有
init()函数 panic?基准测试会在 setup 阶段崩溃,但错误被静默吞掉,只显示 “no benchmarks to run”——检查go test -v -bench=.看详细日志 - Go 1.21+ 默认开启
-count=1,但旧版本可能因缓存导致结果复用,加-count=1强制单次运行更可靠
如何避免 b.N 循环里的常见性能陷阱
b.N 是 Go 自动调整的迭代次数,目标是让单次基准运行时间稳定在 1 秒左右。但如果你在循环里干了不该干的事,b.N 就成了干扰项。
立即学习“go语言免费学习笔记(深入)”;
- 别在循环里做一次性操作:比如每次迭代都
json.Marshal同一个结构体再json.Unmarshal,而没把序列化结果提前算好——CPU 时间全花在重复编码上,不是你真正想测的逻辑 - 别意外逃逸:循环内新建大对象(如
make([]byte, 1024*1024))会导致频繁堆分配,b.ReportAllocs()会暴露这点,此时应考虑复用sync.Pool或栈上小对象 - 注意编译器优化:空循环、未使用的返回值可能被整个删掉。确保关键计算结果至少被
b.DoNotOptimize捕获,或赋给b的字段(如b.ReportMetric) - 并发基准要小心:用
b.RunParallel时,每个 goroutine 拿到的是独立的*testing.B副本,但共享原始b.N总量;别在 parallel fn 里调b.ResetTimer,它只对当前 goroutine 生效
最麻烦的其实是「你以为在测 A,其实主要耗时在 B」——比如测哈希表查找,但 key 是每次 new 出来的字符串,那测的其实是内存分配+字符串构造,不是查找本身。动手前先用 pprof 粗筛热点,比硬调 b.N 有用得多。











