基准测试测的是函数执行耗时与内存分配,而非结果正确性;需赋值防优化、用b.ReportAllocs()查看内存指标、避免提前初始化或缓存干扰。

用 testing.B 测函数返回值的耗时和内存开销
Go 的基准测试不是测“函数是否返回正确值”,而是测“返回这个值花了多少时间、占多少内存”。所以你要先确认函数逻辑正确,再用 testing.B 包裹调用逻辑。重点不是验证结果,而是让 Benchmark 函数不被编译器优化掉——否则测出来全是 0 ns/op。
- 必须把函数调用结果赋给局部变量(哪怕不用),否则 Go 1.21+ 会直接内联并优化掉整个调用
- 避免在
b.ResetTimer()前做初始化(比如构造大 slice),否则初始化时间会被计入基准 - 用
b.ReportAllocs()才能看见allocs/op和B/op
Benchmark 示例:测 strings.ToUpper 和自定义转换的差异
假设你想对比标准库和自己写的字符串转大写性能。注意:不能只写 strings.ToUpper(s),得把结果存下来;也不能在循环外提前算好输入,否则测的是缓存命中率而非真实吞吐。
func BenchmarkStdToUpper(b *testing.B) {
s := "hello world"
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = strings.ToUpper(s) // 必须保留这行,且不能被移除
}
}
func BenchmarkMyToUpper(b *testing.B) {
s := "hello world"
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = myToUpper(s) // 同样必须赋值,哪怕丢弃
}
}
常见误操作:返回值被优化、没重置计时器、误用 b.N
以下写法会导致基准失真:
- 写成
strings.ToUpper("hello")字面量 —— 编译器可能常量折叠,测不到实际执行路径 - 在
for循环前调用b.ResetTimer()—— 计时器重置太晚,前面的 setup 时间被计入 - 手动写
for i := 0; i —— 完全绕过b.N,失去 Go 基准自动调节迭代次数的能力 - 用
fmt.Println或log.Print—— I/O 会严重拖慢结果,且输出干扰go test -bench解析
进阶:测带副作用或依赖外部状态的函数返回值
如果函数依赖随机数、当前时间、文件读取等,不能直接在 Benchmark 里调用——因为每次运行环境不同,结果不可比。正确做法是把“可变输入”抽成参数,在基准中预生成固定数据集:
立即学习“go语言免费学习笔记(深入)”;
func BenchmarkParseJSON(b *testing.B) {
data := []byte(`{"name":"alice","age":30}`)
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var u User
_ = json.Unmarshal(data, &u) // 复用同一份 data,排除 IO 和生成开销
}
}
真正难的不是写 Benchmark 函数,而是确保你测的确实是目标路径——比如想测 map 查找性能,就别在循环里反复 make(map[int]int);想测返回值构造成本,就得把分配和初始化都包进去,而不是只测最后一步赋值。











