一个能跑起来的 go test -bench 必须满足:函数名以 benchmark 开头、接收 *testing.b 参数、放在 _test.go 文件中,并在 b.n 次循环中执行待测逻辑;否则会报 no tests to run 或 benchmarked 0 times。

怎么写一个能跑起来的 go test -bench
Go 的基准测试不是靠手动计时,而是必须用 testing.B 类型、函数名以 Benchmark 开头、放在 _test.go 文件里——缺一不可,否则 go test -bench=. 直接忽略。
常见错误现象:no tests to run 或 benchmarked 0 times,基本是函数签名不对或文件后缀不是 _test.go。
-
Benchmark函数必须接收*testing.B参数,不能是*testing.T - 必须在循环里调用
b.N次待测逻辑,不能自己写for i := 0; i - 如果逻辑里有初始化开销(比如建 map、读配置),要放到
b.ResetTimer()前面,避免计入耗时
func BenchmarkAdd(b *testing.B) {
var x, y int = 1, 2
b.ResetTimer() // 从这行开始计时
for i := 0; i <h3>
<code>go test -bench</code> 输出里的 <code>ns/op</code> 和 <code>B/op</code> 到底怎么看</h3><p><code>ns/op</code> 是每次操作平均纳秒数,数值越小越好;<code>B/op</code> 是每次操作分配的字节数,反映内存压力。两者不总是一致——比如优化了 CPU 但用了更多临时变量,<code>ns/op</code> 下降但 <code>B/op</code> 上升。</p><p>性能影响关键点:GC 频率会随 <code>B/op</code> 显著升高而增加,尤其在高频调用场景下,可能比纯 CPU 耗时更伤整体吞吐。</p>
- 同一函数多次运行,
b.N会自动调整(比如从 100 万涨到 1000 万),确保测量稳定,不用手调 - 加
-benchmem才显示B/op和内存分配次数,不加默认不统计 - 如果
B/op是0,不代表没分配,可能是编译器优化掉了(比如逃逸分析判定变量未逃逸)
为什么 runtime.GC() 有时要手动调用
基准测试默认不触发 GC,但如果被测逻辑本身会触发大量分配(比如反复构造大 slice),不同轮次间堆状态不一致,ns/op 就会抖动甚至越来越差——这时得在每次循环前手动清理。
容易踩的坑:在 for 循环内每轮都调 runtime.GC(),反而把 GC 时间算进基准里;正确做法是只在 b.ResetTimer() 前调一次,或用 b.ReportAllocs() 配合观察趋势。
- 仅当怀疑 GC 干扰结果时才加,日常简单函数不需要
- 加了
runtime.GC()后记得用time.Sleep(2 * time.Millisecond)等 GC 完成,否则可能测到半截状态 - 更好的替代是用
-gcflags="-m"看逃逸分析,从源头减少分配
多个 benchmark 怎么对比才靠谱
直接看单次 go test -bench=. 输出容易误判,因为 CPU 频率波动、后台进程、缓存预热都会影响。真要比,得用 benchstat 工具做统计分析。
兼容性影响:Go 1.21+ 默认启用 GOEXPERIMENT=fieldtrack,会影响某些结构体字段访问的基准结果,跨版本对比前先统一环境。
- 至少跑 3 组,每组用
go test -bench=. -count=5 > old.txt保存原始数据 - 用
benchstat old.txt new.txt看 p-value 和 delta,p 且 <code>delta > 5%才算显著差异 - 别信 “快了 2x”,要看误差范围(
±12%这种),误差超 10% 就该检查机器负载
最常被忽略的是预热:第一次运行永远最慢,benchstat 会自动跳过首条数据,但自己写脚本跑时得手动剔除。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











