靠谱的 json.Marshal Benchmark 需每次迭代新建数据并隔离内存生命周期:调用 b.ReportAllocs() 统计分配,b.ResetTimer() 排除初始化开销,不复用结构体或切片,避免缓存干扰与 GC 抖动。

为什么 encoding/json 的 Benchmark 容易失真
直接用 testing.Benchmark 测 json.Marshal 或 json.Unmarshal 时,常见结果波动大、不可复现,甚至比实际运行快几倍。根本原因是:Go 的 benchmark 默认会复用变量、不强制 GC、忽略内存逃逸路径,而 JSON 序列化对内存分配和 GC 敏感度极高。
- 未重置
b.ResetTimer()前的初始化开销(如构建测试结构体)被计入耗时 - 测试数据复用导致 CPU 缓存命中率虚高,掩盖真实冷启动成本
- 未统计
b.ReportAllocs(),无法判断是否因小对象逃逸引发高频堆分配 - 未控制
GOGC或手动触发runtime.GC(),GC 干扰使耗时抖动剧烈
写一个靠谱的 json.Marshal Benchmark 示例
核心是让每次迭代都生成「新数据」+「强制隔离内存生命周期」。避免复用结构体指针或预分配切片。
func BenchmarkJSONMarshal(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
// 每次迭代构造全新数据,防止编译器优化或缓存复用
data := struct {
ID int `json:"id"`
Name string `json:"name"`
Tags []string `json:"tags"`
Valid bool `json:"valid"`
}{
ID: i,
Name: "user-" + strconv.Itoa(i%1000),
Tags: []string{"go", "json", "perf"},
Valid: true,
}
_, err := json.Marshal(data)
if err != nil {
b.Fatal(err)
}
}
}
- 不用全局变量或闭包捕获的
data,确保每次都是独立栈/堆分配 - 字段值带
i%1000避免字符串 intern 导致的假性优化 - 显式调用
b.ReportAllocs()后,输出会包含Benchmem行,关注allocs/op和B/op
对比 json.Unmarshal 时必须预热字节流
反序列化性能受输入字节流是否已驻留 L1/L2 缓存影响极大。若每次 json.Unmarshal 都从新分配的 []byte 开始,测的是内存拷贝 + 解析,不是纯解析。
- 先用
json.Marshal生成基准字节流,在Benchmark外完成 - 在 benchmark 循环内只做
json.Unmarshal,且用bytes.NewReader或直接传[]byte(避免额外 alloc) - 若结构体含指针或嵌套 map/slice,需确认 unmarshal 是否触发新分配 —— 可通过
unsafe.Sizeof配合runtime.ReadMemStats抽样验证
var benchData []byte
func init() {
data := struct{ Name string }{Name: "test"}
var err error
benchData, err = json.Marshal(data)
if err != nil {
panic(err)
}
}
func BenchmarkJSONUnmarshal(b *testing.B) {
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
var v struct{ Name string }
err := json.Unmarshal(benchData, &v)
if err != nil {
b.Fatal(err)
}
}
}
真正影响线上性能的三个隐藏点
跑出漂亮 benchmark 数字不等于线上快。以下三点常被忽略,但决定 JSON 序列化是否成为瓶颈:
立即学习“go语言免费学习笔记(深入)”;
-
json.RawMessage能跳过中间解析,但若后续仍要解成 struct,只是延迟了开销,且增加维护复杂度 - struct tag 里写
json:",omitempty"会导致反射判断逻辑变重,尤其字段多时,实测比固定字段慢 8%~15% - 使用
json.Encoder/json.Decoder处理流式数据时,底层bufio.Writer缓冲区大小(默认 4KB)若远小于 payload,会频繁 syscall,此时应显式传入bufio.NewWriterSize(w, 64*1024)
别只盯着 ns/op,先看 allocs/op 是否稳定、GCPause 是否突增、pprof 的 runtime.mallocgc 占比 —— 这些才是压测时真正该盯住的地方。











