确认瓶颈真在序列化需用pprof看火焰图:若reflect.value.interface与encoding/json.(*encodestate).marshal合计占cpu超30%,才是真瓶颈;否则多为上游数据构造问题,如循环拼map[string]interface{}或滥用json.rawmessage。

确认瓶颈真在序列化,而不是上游数据构造
别一看到慢就换库或改结构——90% 的“序列化慢”其实是上游代码在反复拼 map[string]interface{} 或滥用 json.RawMessage。用 pprof 看火焰图,只看两个函数占比:reflect.Value.Interface 和 encoding/json.(*encodeState).marshal。如果加起来 json.Marshal,而在你组装数据的逻辑里。
常见误操作:
- 循环中不断
append到[]map[string]interface{},每次分配新map和字符串 - 把
json.RawMessage当缓存用,但反序列化时仍要 decode 一遍,没省事 - 用
time.Now().UTC().Format(...)手动转时间字符串再塞进 map,绕过类型安全还多一次内存分配
easyjson 不是装了就快,调用路径必须严格对齐
easyjson 能绕过反射,但前提是它生成的代码被真正调用。很多人跑完 easyjson -all user.go 就以为万事大吉,结果还在走原生 json.Marshal(&u)。
正确姿势:
- 结构体上方必须独占一行写
//easyjson:json,不能有空格、不能混写其他注释 - 生成的
user_easyjson.go必须和原文件同包,且一起编译(go build 时不能漏掉) - 调用时不能写
json.Marshal(&u),得用u.MarshalJSON()或easyjson.Marshal(&u) - 含
interface{}字段会 panic,不是报错——要么提前转成具体类型,要么字段声明为*interface{}
jsoniter 加速失效的三个典型配置错误
jsoniter 兼容标准库导入路径,但默认行为跟原生一样慢。加速靠的是复用全局 jsoniter.API 实例,而不是每次调用都重配。
踩坑点:
- 在 HTTP handler 里每次写
jsoniter.Config{}.Froze(),等于每次初始化反射缓存,比原生还慢 - 没设
Unsafe = true(可信数据场景下),少掉 20–40% 性能提升 - 字段含
time.Time却没注册自定义 encoder,结果输出 RFC3339 字符串,前端解析失败,误判为“慢”
推荐初始化方式:var json = jsoniter.ConfigCompatibleWithStandardLibrary,全局只定义一次,后面直接用 json.Marshal。
真正省时间的,往往是绕过序列化本身
很多接口吞吐上不去,根本不是 Marshal 慢,而是你在 hot path 上做了太多本可避免的转换。比如高频透传 JSON,硬要先 Unmarshal 成 struct 再 Marshal 回去。
更高效的做法:
- 上游确认是合法 JSON?直接用
[]byte透传,连json.RawMessage封装都省了 - 固定结构响应?手写
w.Write([]byte{...}),比json.Encoder还快,实测吞吐翻 3 倍 - 批量写入?用
sync.Pool管理bytes.Buffer,避免逃逸到堆上触发 GC - 内部服务通信?直接切
protobuf,1MB 数据序列化从 12.4ms 降到 1.8ms,内存分配从 47 次降到 9 次
最易被忽略的一点:proto.Marshal 预分配 buffer 要用 MarshalAppend + proto.Size,而不是 make([]byte, proto.Size(msg)) —— 后者底层数组会被覆盖,可能越界或残留脏数据。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











