导语:JSON 是每个 Go 服务的性能暗礁
encoding/json 太好用了,以至于几乎没人注意它的开销——直到压测时发现序列化占了 40% 的 CPU。JSON 反射序列化的成本藏在反射遍历结构体字段、分配大量临时对象、每个 int/string 都要再编码里。本文从原理出发,给你一套"先用标准库优化、再考虑加速库"的分层打法。
一句话总结:JSON 性能的三大杠杆是——减少反射(预编译模板)、减少分配(复用 buffer/对象)、减少不必要字段(tag + 流式);先榨干标准库,量再大再上 jsoniter/easyjson。
1. 为什么 encoding/json 慢:反射成本
1.1 每次 Marshal 都反射
type Order struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Amount float64 `json:"amount"`
Items []Item `json:"items,omitempty"`
}
// 每次 json.Marshal 都会:
// 1. reflect.TypeOf 遍历 struct 所有字段
// 2. 对每个字段按 tag 解析名字、找方法
// 3. 为中间结果分配大量 buffer
// 4. 数字转字符串、字符串转义……逐字段组装
data, _ := json.Marshal(order)
1.2 数字量级参考(百万次序列化对比)
| 方案 | 每 op 耗时 | 分配 | 说明 |
|---|---|---|---|
| encoding/json | ~1.0µs | ~800 B/op | 反射 + 分配 |
| 手写 String() 拼接 | ~0.2µs | ~0 | 零反射、零分配 |
| easyjson(生成代码) | ~0.25µs | ~几十 B | 生成专用编码器 |
| jsoniter(反射优化) | ~0.5µs | ~400 B | 缓存类型信息 |
一句话总结:标准库的慢主要是反射和分配;凡是"把反射换成预编译",速度就能翻倍到几倍。
2. 标准库先用到位
2.1 Marshaler / Unmarshaler 接口
// 对特殊类型实现接口,跳过反射
type OrderID int64
func (id OrderID) MarshalJSON() ([]byte, error) {
// 自定义编码(比如 时间戳/格式化)
return []byte(strconv.FormatInt(int64(id), 10)), nil
}
2.2 复用 encoder 与 buffer
// 别每次申请新 bytes.Buffer
var bufPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func marshalToPool(v interface{}) []byte {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
if err := json.NewEncoder(buf).Encode(v); err != nil {
return nil
}
// Encoder 自动加 \n,去尾部换行
return bytes.TrimSuffix(buf.Bytes(), []byte("\n"))
}
2.3 tag 与结构设计优化
// ✅ 用 omitempty 减少空字段输出
// ✅ 别序列化整个嵌套对象,用自定义方法投影
type OrderView struct {
ID int64 `json:"id"`
... // 只投影需要的字段
}
// ❌ 避免 map[string]interface{}(反射最贵 + 无法优化)
// ❌ 避免 interface{} 字段(丢失类型,序列化最慢)
一句话总结:标准库优化的重点是——实现接口绕反射、sync.Pool 复用 buffer、用具体类型替代 interface{}、精简字段。
3. jsoniter:反射的缓存化加速
3.1 用法几乎零成本迁移
import jsoniter "github.com/json-iterator/go"
var json = jsoniter.ConfigCompatibleWithStandardLibrary
// 之后的 json.Marshal / json.Unmarshal 用法与标准库一致
// 但内部缓存了解码器/编码器,避免反复反射
data, err := json.Marshal(order)
var order2 Order
json.Unmarshal(data, &order2)
3.2 原理与适用
jsoniter 做了什么:
- 首次遇到某类型时,反射出编码/解码器并缓存
- 之后用缓存的能力,不再逐次反射
- 内部用 unsafe 减少拷贝、复用内存
适用:
- 结构类型固定、要反复序列化(API 网关、HTTP handler)
- 想零代码迁移,直接换 import 即可
不适用:
- 类型反射动态变化极多(尽量少用 interface{} 路径)
- 追求极致性能时不如 easyjson 的代码生成
一句话总结:jsoniter 是"标准库的即插即用加速器"——接口不变、性能提升,适合多数 HTTP 服务直接替换。
4. easyjson:代码生成的极致方案
4.1 安装与生成
# 安装生成器
go get -u github.com/mailru/easyjson/...
# 在结构体上标记,然后生成
go run github.com/mailru/easyjson -all ./model.go
# 生成 model_easyjson.go,包含 MarshalJSON / UnmarshalJSON
//easyjson:json
type Order struct {
ID int64 `json:"id"`
UserID int64 `json:"user_id"`
Amount float64 `json:"amount"`
Items []Item `json:"items,omitempty"`
}
4.2 使用与收益
// 直接调用生成的快路径
data, _ := easyjson.Marshal(order)
var out Order
easyjson.Unmarshal(data, &out)
// 性能:约标准库 2~4 倍,分配减少 90%+
// 本质:生成面向该 struct 的手写 marshal 代码,零反射
一句话总结:easyjson 用"生成专用代码"换最高性能,适合流量极大、结构稳定的核心路径。
5. Streaming:大 JSON 的流式解析
5.1 为什么需要流式
大 JSON(几 MB ~ 几百 MB)一次性 Unmarshal 的代价:
- 整块载入内存,GC 压力大
- 解析出完整对象树,即使只关心个别字段
Streaming 思路:
- json.Decoder 逐 token 解析,不建整棵对象树
- 边读边处理,内存 O(字段数) 而非 O(全文件)
5.2 Decoder 流式读取
dec := json.NewDecoder(reader)
for dec.More() { // 数组还有更多元素
var item Record
if err := dec.Decode(&item); err != nil {
break
}
process(item) // 逐条处理,内存可控
}
5.3 流式只取目标字段
// 大型对象里只想拿某个 key,用 Decoder.Token 跳过无关部分
dec := json.NewDecoder(reader)
// 走到目标字段再 Decode,其余 token 直接跳过
for dec.Token() != nil {
// ...
}
一句话总结:大 JSON 用 json.Decoder 逐条处理,内存与 GC 压力骤降;别图省事整块 Unmarshal。
6. 选型与避坑速查
| 场景 | 推荐 | 理由 |
|---|---|---|
| 默认 / 通用 | encoding/json | 兼容、稳 |
| HTTP 服务高频 | jsoniter | 零迁移提性能 |
| 极致性能核心路径 | easyjson | 生成代码 |
| 大 JSON 日志/导入 | json.Decoder 流式 | 内存可控 |
| 跨语言严格契约 | 保持 tag 稳定 | 避免破坏兼容 |
避坑清单:
□ 别用 map[string]interface{} 存业务结构(反射最贵 + 无类型安全)
□ tag 变更要平滑,避免线上格式破坏
□ 大字段用流式/分片,别一次载入
□ 复用 buffer 用 sync.Pool,注意 Reset
□ 数字精度丢失:金额用 string/decimal,别用 float
□ 忽略错误:Unmarshal 失败时静默返回零值,务必处理 err
□ 高频路径用 easyjson 前先压测对比,别盲目引入
7. 总结
JSON 性能优化的路径是从"通用"走向"专用":
| 杠杆 | 手段 | 收益 |
|---|---|---|
| 减反射 | 实现接口 / jsoniter / easyjson | 2~4× |
| 减分配 | sync.Pool、复用 encoder | 内存下降 |
| 减体积 | omitempty、投影精简字段 | 传输/解析双降 |
| 减内存 | 流式 Decoder | 大 JSON 可控 |
落地记住五件事:先优化标准库用法(接口+buffer)、再换 jsoniter、极致路径上 easyjson、大 JSON 走流式、tag 保持稳定。把"反射+分配"这两座大山削掉,JSON 就不再是服务性能的瓶颈。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。