Go 的 Benchmark 函数必须命名为 BenchmarkXXX(*testing.B),调用 b.N 循环执行被测逻辑,初始化代码置于 b.ResetTimer() 前,调用 b.ReportAllocs() 获取内存分配统计。

Go 自带的 testing 包支持基准测试(Benchmark),不需要额外安装工具,但必须理解它不是模拟并发用户请求的“压测”,而是测量单个函数在受控条件下的执行性能。
如何写一个合法的 Benchmark 函数
Go 的 go test 只识别形如 BenchmarkXXX(*testing.B) 的函数。它和普通测试函数不同:不能用 t.Fatal,必须调用 b.N 控制循环次数,且被测逻辑需放在 b.ResetTimer() 和 b.ReportAllocs() 附近以排除初始化干扰。
-
b.N是框架自动调整的迭代次数,不是你手动指定的“跑 1000 次”——它会根据首次运行耗时动态扩增,确保总耗时稳定在 1 秒左右 - 若被测逻辑含初始化开销(如建 map、读配置),应把初始化放在
b.ResetTimer()之前,否则会污染结果 - 加
b.ReportAllocs()才能在结果中看到内存分配统计(B/op和allocs/op)
func BenchmarkAdd(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
_ = add(1, 2)
}
}
运行 benchmark 并解读关键指标
用 go test -bench=. 运行,默认只显示耗时(ns/op)。加上 -benchmem 才显示内存数据;加上 -count=3 可跑多次取平均值,避免单次抖动影响判断。
- 输出中
1000000000表示跑了 10 亿次,1.23 ns/op是每次平均耗时——数值越小越好 -
2 B/op表示每次调用分配了 2 字节内存,0 allocs/op表示没触发堆分配——这对高频函数很关键 - 如果看到
500000000 3.45 ns/op,说明函数较慢,Go 自动减少了b.N次数来保证单轮不超时
为什么 Benchmark 不能替代真实压测
testing.B 是单 goroutine 循环调用,不涉及网络、I/O 等阻塞操作,也不模拟多客户端并发竞争资源。它测的是“理想路径下函数的纯 CPU 性能”。比如:
立即学习“go语言免费学习笔记(深入)”;
- 测
json.Marshal可以,但测 HTTP handler 不行——handler 里有 net/http 的调度、TLS 握手、连接复用等不可控变量 - 测
sync.Map.Load有意义,但测整个 API 接口的 QPS 就会严重高估——实际瓶颈往往在数据库连接池或锁争用上 - 若想模拟 1000 并发请求,得用
ab、hey或vegeta这类外部工具打真实 endpoint
常见陷阱:误用 Benchmark 导致结果失真
最容易踩的坑是让编译器优化掉被测逻辑,或者把副作用留在循环外。
- 写成
result := add(1,2); for i:=0; i→ 整个循环被优化为空,结果是 0 ns/op,毫无意义 - 忘记加
_ =或用result,导致 Go 认为返回值未使用而内联/消除调用 - 在循环里做 I/O(如
fmt.Println)或 sleep → 结果反映的是系统调用耗时,不是函数本身性能 - 用
time.Now()手动计时 → 绕过testing.B的自适应机制,且纳秒级时间获取本身有开销
真正要定位线上性能瓶颈,得先用 pprof 抓 CPU profile,再针对 hot path 写精准 benchmark;盲目对整个 handler 跑 benchmark,往往只验证了“这段代码确实挺快”,却掩盖了真正的慢点。










