JSON性能瓶颈80%源于反射,easyjson/go-json通过编译期生成代码消除反射,jsoniter需手动启用缓存与复用实例才能提速。

标准库 json.Unmarshal 和 json.Marshal 的性能瓶颈,80%以上来自反射——不是你结构体写得不对,而是每次调用都得重新查字段、判类型、解标签、做 nil 检查。绕不开反射,就别谈“优化”,只谈“妥协”。
为什么你的 pprof 显示 reflect.Value.Interface 占 CPU 40%+
这不是偶然。当结构体字段 ≥8 个、嵌套深度 ≥3,或含 interface{}、*string、json.RawMessage 时,标准库会进入最慢路径:逐字段反射访问 + 动态类型断言 + 多次小对象分配。更隐蔽的是,map[string]interface{} 输入会强制走通用解析器,完全放弃结构体预知优势。
- 字段名不匹配(比如 tag 写成
json:"user_id"但结构体字段叫UserID)会导致 fallback 到反射查找 -
time.Time字段若没配json:"time_local,string"或自定义UnmarshalJSON,会触发额外字符串转换和time.Parse调用 - 未导出字段(小写首字母)永远无法被标准库或 easyjson/go-json 静态绑定,只能反射
easyjson:编译期生成代码,彻底砍掉反射
它不运行时查字段,而是在 easyjson -all models.go 时,把每个字段的序列化/反序列化逻辑硬编码进 models_easyjson.go,变成纯结构体字段访问 + strconv.AppendInt 这类零分配操作。
- 结构体上方必须独占一行写
//easyjson:json,否则不生成 - 字段必须导出(大写首字母)且带显式
json:"field"tag,如ID int `json:"id"` - 禁用
interface{}字段;若需动态字段,改用json.RawMessage或预定义子结构体 - 生成后调用
u.MarshalJSON(),而非json.Marshal(&u);框架如 Gin 仍走c.ShouldBindJSON(&u),只要u类型有生成方法即可自动适配
go-json:比 easyjson 更激进的编译期绑定
github.com/goccy/go-json 在构建时直接生成字段偏移+类型信息表,连 reflect.StructField 都不碰。实测对固定结构 JSON,Unmarshal 耗时降 40%~60%,内存分配减半以上——但前提是别让它 fallback。
立即学习“go语言免费学习笔记(深入)”;
- 导入必须替换为
import json "github.com/goccy/go-json",不能混用encoding/json - 所有待解析字段必须导出,且
json:tag 不可省略;omitempty可保留,不影响性能 - 避免嵌套超 4 层,或结构体中含
interface{};否则退化为反射路径,优势归零 - 流式解析优先用
json.NewDecoder(r.Body).Decode(&v),它复用底层 buffer,不 malloc 中间[]byte
jsoniter:兼容性优先,但必须手动启用加速
它是唯一能“零修改代码”切换的方案,但默认配置下几乎不提速——因为没开类型缓存、没禁用安全检查、没复用实例。
- 全局只创建一次
jsoniter.API实例,别在 handler 里反复jsoniter.Config{}.Froze() - 显式注册类型编码器:
jsoniter.RegisterTypeEncoder("User", &userEncoder{}),否则仍走反射 - 可信数据源可设
Unsafe=true,跳过 UTF-8 校验和边界检查,提速 20%~40% - 注意:它默认对字符串做 HTML 转义(
"→"),和前端预期不符时需手动覆盖 encoder
真正难的不是选哪个库,而是确认瓶颈真在 JSON 层——很多服务换完 easyjson 吞吐没涨,结果发现是上游循环拼 map[string]interface{} 或反复 json.RawMessage 二次解析。先跑 pprof,盯住 reflect.Value.Interface 和 runtime.mallocgc,再动手改代码。



















