json.Marshal/Unmarshal 慢因反射开销大、内存分配频繁;easyjson 通过编译期生成无反射代码提升2–5倍吞吐、减少90%+ GC;合理使用 json.RawMessage 和复用 bytes.Buffer 进一步优化。

为什么 json.Marshal 和 json.Unmarshal 会慢
Go 标准库的 encoding/json 包在运行时大量依赖反射(reflect),每次序列化/反序列化都要动态检查字段名、类型、标签(json:"name")、可导出性,甚至做字符串拼接和 map 查找。这意味着:结构体越深、字段越多、嵌套越复杂,性能损耗越明显;尤其在高频 API 场景下,CPU 时间常被反射和内存分配吃掉大半。
常见现象包括:
- pprof 显示
reflect.Value.Interface或encoding/json.(*encodeState).marshal占用高 CPU - GC 频繁,
runtime.mallocgc调用次数飙升(因反复分配临时[]byte和map[string]interface{}) - 小对象(如
struct{ID int `json:"id"`})单次耗时看似不高,但 QPS 上万时累积开销不可忽视
用 easyjson 替代标准 json 包(零反射)
easyjson 在编译期生成专用的 MarshalJSON / UnmarshalJSON 方法,完全绕过反射。它不改结构体定义,只需加一行注释 + 运行代码生成命令。
实操步骤:
立即学习“go语言免费学习笔记(深入)”;
- 给结构体加上
//easyjson:json注释(必须独占一行) - 运行
easyjson -all models.go(假设结构体在models.go) - 生成文件如
models_easyjson.go,其中含无反射的序列化逻辑 - 调用时仍用
easyjson.Marshal或直接调用生成的方法(如v.MarshalJSON())
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
//easyjson:json
生成后,User.MarshalJSON() 是纯字段访问 + strconv 拼接,没有 reflect,也没有 interface{} 类型断言。实测同等结构体,吞吐量可提升 2–5 倍,GC 分配减少 90%+。
避免 json.RawMessage 误用导致二次解析
json.RawMessage 常被用来“跳过中间解析”,比如把某个嵌套 JSON 字段暂存为字节流,后续按需解析。但它不是银弹——若后续仍要频繁解析同一段 RawMessage,等于把延迟解析变成了重复解析,反而更慢。
正确用法场景:
- 字段内容不确定,且只在特定分支才解析(如 webhook payload 中的
data字段仅对某类事件有意义) - 需要透传原始 JSON 给下游,不修改结构(如代理 API)
错误用法:
- 每次 HTTP 请求都对同一个
json.RawMessage调用json.Unmarshal—— 应缓存解析结果(如用sync.Once或首次访问时 lazy 解析) - 用
json.RawMessage存储小对象(如{"status":"ok"}),不如直接定义结构体 + 标准反序列化,省去 copy 和边界检查开销
手动控制内存:复用 bytes.Buffer 和预分配切片
标准 json.Marshal 每次都 new 一个 []byte,而 json.Unmarshal 也会为 map/slice 分配新底层数组。在服务长期运行中,这会导致大量小对象堆积 GC 压力。
可优化点:
- 用
bytes.Buffer复用底层[]byte:声明为字段或从sync.Pool获取 - 对已知大小的结构体,预估 JSON 字节数并调用
buf.Grow(n),避免多次扩容 - 反序列化时,若目标 slice 容量已知(如日志条目固定最多 100 条),先
make([]T, 0, 100)再传入json.Unmarshal,减少 append 扩容
示例(复用 buffer):
var bufPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func MarshalUser(u *User) ([]byte, error) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
if err := json.NewEncoder(buf).Encode(u); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
注意:json.Encoder 比 json.Marshal 更适合复用场景,它直接写入 io.Writer,避免中间 []byte 分配;但需注意 Encode 会自动加换行,如需紧凑 JSON,用 buf.Bytes() 后手动 trim 换行或改用 json.Compact 处理。
真正难的不是选哪个库,而是判断哪部分 JSON 流量最热、结构最稳——那里才值得上代码生成或内存池。其他地方,标准库够用,过早优化反而增加维护成本。











