靠谱的 json.Marshal Benchmark 需每次迭代新建数据并隔离内存生命周期:调用 b.ReportAllocs() 统计分配,b.ResetTimer() 排除初始化开销,不复用结构体或切片,避免缓存干扰与 GC 抖动。
encoding/json 的 Benchmark 容易失真直接用 testing.Benchmark 测 json.Marshal 或 json.Unmarshal 时,常见结果波动大、不可复现,甚至比实际运行快几倍。根本原因是:Go 的 benchmark 默认会复用变量、不强制 GC、忽略内存逃逸路径,而 JSON 序列化对内存分配和 GC 敏感度极高。
b.ResetTimer() 前的初始化开销(如构建测试结构体)被计入耗时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 外完成json.Unmarshal,且用 bytes.NewReader 或直接传 []byte(避免额外 alloc)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 序列化是否成为瓶颈:
json.RawMessage 能跳过中间解析,但若后续仍要解成 struct,只是延迟了开销,且增加维护复杂度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 占比 —— 这些才是压测时真正该盯住的地方。