大家好,我是阿蒙。
做 Go 后端这两年,抓 CPU 瓶颈十次有八次能看到 runtime.mallocgc 和 reflect.*,点开火焰图一看,全是在那默默倒腾 JSON 序列化。
官方标准库 encoding/json 确实省心,但在高 QPS 或网关服务下,它的反射开销和频繁的堆内存分配直接就是 CPU 刺客。今天不整虚的,直接拉几个主流库拉个 Benchmark,顺便聊聊实际项目里怎么选型和压榨性能。
1. 选手就位
这次拉来对比的库:
encoding/json:官方标杆,纯反射,稳定兼容性最好。json-iterator/go:老牌替代者,号称 100% 兼容标准库,利用 unsafe 和编译期缓存加速。bytedance/sonic:字节跳动开源的性能怪兽,基于 JIT 和 SIMD 指令集优化。goccy/go-json:纯 Go 实现,不用代码生成,靠 AST 分析和寄存器级优化。mailru/easyjson:代码生成流,靠提前生成序列化代码彻底干掉反射。
2. 基准测试(Benchmark)
定义一个典型的业务结构体,包含嵌套 Struct、切片、基本类型和时间:
go
type User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Roles []string `json:"roles"`
IsActive bool `json:"is_active"`
ExtraInfo MetaData `json:"extra_info"`
CreatedAt time.Time `json:"created_at"`
}
type MetaData struct {
IP string `json:"ip"`
Device string `json:"device"`
LoginDays int `json:"login_days"`
}
测试环境:Apple M2 Pro (arm64), Go 1.22.0。
核心 Benchmark 代码:
go
func BenchmarkMarshal(b *testing.B) {
u := mockUser()
b.Run("stdlib", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(u)
}
})
b.Run("json-iterator", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = jsoniter.Marshal(u)
}
})
b.Run("go-json", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = gojson.Marshal(u)
}
})
b.Run("sonic", func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, _ = sonic.Marshal(u)
}
})
}
实测数据输出:
text
goos: darwin
goarch: arm64
pkg: bench-json
BenchmarkMarshal/stdlib-10 1258603 945.2 ns/op 288 B/op 2 allocs/op
BenchmarkMarshal/json-iterator-10 3154820 380.1 ns/op 256 B/op 2 allocs/op
BenchmarkMarshal/go-json-10 3921045 305.6 ns/op 240 B/op 1 allocs/op
BenchmarkMarshal/sonic-10 6842100 175.4 ns/op 240 B/op 1 allocs/op
BenchmarkUnmarshal/stdlib-10 610234 1950.8 ns/op 432 B/op 9 allocs/op
BenchmarkUnmarshal/json-iterator-10 1845120 648.2 ns/op 256 B/op 4 allocs/op
BenchmarkUnmarshal/go-json-10 2105430 570.0 ns/op 336 B/op 5 allocs/op
BenchmarkUnmarshal/sonic-10 3890124 308.2 ns/op 240 B/op 2 allocs/op
看数字说话:
- 序列化上,
sonic相比标准库快了 5.3 倍,内存分配从 2 次降到 1 次。 - 反序列化上,标准库 allocs 较多(9 次),
sonic仅 2 次,耗时缩短到原来的 1/6。 go-json和json-iterator表现也很均衡,基本能稳定在标准库的 2.5 ~ 3 倍速度。
3. 各方案的坑与选型思考
不能只看跑分无脑上,实际生产环境有各种边界问题:
(1) bytedance/sonic:追求极致性能的首选
- 优点:JIT 编译 + SIMD 向量指令,处理大 JSON 和长文本时性能极其变态。
- 缺点/坑:
- 只支持
AMD64和ARM64架构,不支持386、mips等边缘设备。 - 首次解析有 JIT Warmup 耗时。
- 严重依赖
unsafe,Go 跨大版本升级时偶尔会有底层 ABI 兼容风险。
- 只支持
- 适用场景:高流量网关、核心 RPC 中间件、微服务核心链路。
(2) goccy/go-json 或 json-iterator/go:稳妥的平替方案
- 优点:几乎是标准库的 1:1 无缝替换,直接替换 import 就能提速 2~3 倍。跨平台兼容性比
sonic好。 - 缺点:
json-iterator近期维护活跃度有所降低;复杂 tag 行为边缘情况下与标准库有细微差别。 - 适用场景:老项目不想大改代码,需要立竿见影提升性能。
(3) mailru/easyjson:确定性最高的极致方案
- 优点:提前静态生成代码,无运行时反射,单核吞吐极高且零 JIT 开销。
- 缺点:开发体验差,改了结构体必须
go generate,CI/CD 链路繁琐。 - 适用场景:协议极其固定、调用频次极高、平台架构复杂的项目。
4. 代码层面的性能压榨技巧
就算换了库,写业务代码不注意依然会有性能泄漏,这里提三个常用技巧:
A. 避免 map[string]interface{} 反序列化
直接反序列化到 map[string]any 会导致每个 key 和 value 都在堆上分配内存。明确定义 Struct,哪怕字段多一点,性能也远好于 Map。
B. 善用 sync.Pool 复用编码 Buffer
针对频繁 Marshal 的场景,避免每次都产生新的 []byte 切片:
go
var bufPool = sync.Pool{
New: func() any {
return bytes.NewBuffer(make([]byte, 0, 1024))
},
}
func MarshalWithPool(v any) ([]byte, error) {
buf := bufPool.Get().(*bytes.Buffer)
buf.Reset()
defer bufPool.Put(buf)
enc := sonic.ConfigFastest.NewEncoder(buf)
if err := enc.Encode(v); err != nil {
return nil, err
}
// 拷贝出来或直接使用 buf.Bytes()
res := make([]byte, buf.Len())
copy(res, buf.Bytes())
return res, nil
}
C. 大对象局部解析用 json.RawMessage
一个巨大的 Payload,如果服务只关心其中的 header 字段,不要把整个 Payload 全部反序列化:
go
type BigPayload struct {
Header Header `json:"header"`
Content json.RawMessage `json:"content"` // 延迟解析或透传,避免无意义的遍历
}
5. 阿蒙的选型总结
- 新启动的高并发服务 / 内部服务:直接上
sonic,注意 target 架构是 x86_64 或 arm64 即可。 - 跨平台需求 / 容器环境复杂:优先选
goccy/go-json,纯 Go 生态内兼容性最好。 - 日常 CRUD / 低 QPS 内部工具:直接
encoding/json,别折腾依赖,稳定第一。
性能优化先拿 pprof 定位瓶颈,如果 JSON 占比确实超过 15% 以上,换个序列化引擎是最划算的“免费午餐”。
#Golang
#后端
#工具
许可协议:CC BY-NC 4.0
更新于 13 小时前
觉得文章有帮助?点个赞吧!
0 条评论


