Go benchmark中禁用log.Fatal或panic,应使用b.Error()或b.Fatal()报告错误;setup阶段错误用b.Fatal()提前退出,循环内错误用b.Error()记录后continue,避免中断整个测试流程。

Go benchmark 中不能用 log.Fatal 或 panic 捕获错误
Go 的 testing.B 函数运行在受控环境中,一旦触发 panic 或调用 log.Fatal,整个基准测试会立即终止,且不报告任何结果(甚至不显示 BenchmarkXXX 已执行)。这不是“捕获错误”,而是让测试崩溃。
- 正确做法是:所有错误必须显式检查并用
b.Error()或b.Fatal()报告(仅限失败时) -
b.Fatal()会标记该次 benchmark 失败并停止当前函数,但不会中断整个go test -bench流程 - 若错误不影响性能逻辑(例如初始化失败),应在
BenchmarkXxx开头处理,避免进入b.ResetTimer()后才出错
如何在 testing.B 中安全调用可能出错的函数
典型场景是被测函数返回 (result, error),比如 json.Marshal、http.Post 模拟或数据库查询。直接忽略错误会导致结果不可信;用 if err != nil { panic(err) } 会让 benchmark 崩溃。
- 把错误检查放在
b.ResetTimer()之前 —— 如果 setup 阶段就失败,说明测试环境有问题,应提前退出 - 如果错误发生在循环体内(即每次迭代都可能出错),必须判断并跳过本次迭代,或用
b.StopTimer()+b.StartTimer()隔离错误处理开销 - 不要在循环中调用
b.Fatal(),它会中断整个 benchmark 迭代;改用b.Error()记录后continue
func BenchmarkJSONMarshal(b *testing.B) {
data := map[string]int{"x": 1, "y": 2}
// setup 阶段检查
if _, err := json.Marshal(data); err != nil {
b.Fatal("setup failed:", err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
out, err := json.Marshal(data)
if err != nil {
b.Error("marshal failed at iteration", i, ":", err)
continue // 跳过本次,不计入计时
}
_ = out
}
}
go test -bench 不显示错误详情?检查是否漏了 b.Error() 调用
默认情况下,benchmark 只输出耗时和内存分配数据,b.Error() 的内容不会自动打印,除非加上 -benchmem -v 参数。很多开发者以为“没报错”其实是错误被静默吞掉了。
- 运行 benchmark 时务必加
-v:go test -bench=. -benchmem -v -
b.Error()和b.Fatal()的输出只在失败时显示;如果 benchmark 成功完成(哪怕中间有b.Error()),也不会中断,但错误信息仍会出现在终端 - 注意区分:
b.Error()是记录错误但继续跑完所有b.N次;b.Fatal()是立刻终止当前 benchmark 函数
错误处理本身会影响 benchmark 结果,需要隔离测量
真实代码里错误分支往往比正常路径慢得多(比如网络超时、磁盘 I/O 失败重试),直接把错误处理混进主循环会严重污染 ns/op 数据。你测的不是“成功路径性能”,而是“平均错误混合路径性能”。
- 如需单独测错误路径性能,应写独立 benchmark,用构造的错误输入触发(例如传入非法 JSON 字节流给
json.Unmarshal) - 避免在主 benchmark 循环中做条件分支判断错误类型;错误应是异常情况,不该出现在热路径
- 如果业务逻辑强制要求每轮都 check error(比如 retry loop),那 benchmark 就该反映这个真实开销 —— 但要确保错误率可控(例如固定 1%),否则结果抖动太大
b.N 是动态调整的,一旦某次迭代因错误导致耗时剧增,整个 b.N 可能被大幅下调,最终你看到的 ns/op 其实来自极少量样本,且无法复现**。务必先用小规模 -benchtime=10ms 验证逻辑稳定再拉长测试时间。











