Benchmark函数需满足三条件:名以Benchmark开头、参数为*testing.B、置于_test.go文件;b.N由框架动态设定,不可手动固定。

Go 的 Benchmark 函数不是“随便写个循环就能比性能”的工具,它自带计时、迭代控制和结果归一化机制;直接手写 time.Now() 测单次耗时,既不可靠,也无法和 go test -bench 生态协同。
如何正确声明和运行一个 Benchmark 函数
必须满足三个硬性条件:函数名以 Benchmark 开头、参数类型为 *testing.B、放在 _test.go 文件中。Go 测试框架会自动识别并驱动它,b.N 是框架动态调整的执行次数(通常远大于 1),你不能手动设固定值或用 for i := 0; i 替代。
常见错误:把 Benchmark 放在普通 .go 文件里,或参数写成 testing.T —— 这会导致 go test -bench=. 完全不识别该函数。
func BenchmarkMapAccess(b *testing.B) {
m := map[string]int{"key": 42}
for i := 0; i < b.N; i++ {
_ = m["key"]
}
}
为什么不能在 Benchmark 循环里做初始化或分配内存
b.N 次循环会被重复执行,如果在循环内创建 map、slice 或调用 make,这些开销会混入被测逻辑,导致结果虚高。Go 的基准测试默认统计的是每次操作的纳秒级平均耗时(ns/op),任何额外分配都会污染这个指标。
立即学习“go语言免费学习笔记(深入)”;
正确做法是把初始化提到循环外,必要时用 b.ResetTimer() 排除冷启动影响(比如首次 GC 或 JIT 编译延迟):
- 初始化代码写在
for循环之前 - 若初始化本身较重且不属于待测逻辑,可在循环前调用
b.ResetTimer() - 避免在循环中调用
fmt.Println、log.Print等 I/O 操作
func BenchmarkJSONUnmarshal(b *testing.B) {
data := []byte(`{"name":"alice","age":30}`)
var u map[string]interface{}
b.ResetTimer() // 忽略上面变量声明和 data 构造的耗时
for i := 0; i < b.N; i++ {
json.Unmarshal(data, &u)
}
}
如何对比多个实现的性能差异
用统一前缀(如 BenchmarkFoo_V1 / BenchmarkFoo_V2)命名不同版本,并确保它们共享相同输入、不做缓存、不互相干扰。Go 的 go test -bench=. 会自动并列输出所有匹配函数的 ns/op 和内存分配(B/op、allocs/op),这是判断优劣的核心依据。
注意点:
- 不要只看
ns/op绝对值,关注相对差值(比如 V2 比 V1 快 2.3×) -
B/op高往往意味着更多堆分配,可能引发 GC 压力,即使ns/op略低也不一定更优 - 加
-benchmem参数才能显示内存分配统计 - 用
-count=5多次运行取中位数,避免单次抖动干扰
func BenchmarkStringConcat_V1(b *testing.B) {
s := "hello"
for i := 0; i < b.N; i++ {
_ = s + s
}
}
func BenchmarkStringConcat_V2(b *testing.B) {
s := "hello"
for i := 0; i < b.N; i++ {
_ = strings.Repeat(s, 2)
}
}
真正难的是让两次 Benchmark 在语义等价的前提下隔离变量——比如切片预分配长度是否一致、map 是否预先 make 到相同容量、是否无意中复用了指针导致逃逸分析失效。这些细节不显眼,但足以让 ns/op 差异失去可比性。











