Go JSON 编解码慢如何区分 CPU 和分配问题
来源:17golang原创
时间:2026-10-14 06:37:08 102浏览 收藏
Go 的 JSON 编解码慢,不能只看一条 ns/op 就断定是 CPU 问题。真正有用的判断是把同一份输入拆成三组信号:每次耗时、每次分配字节数、每次分配对象数,再用 CPU profile 和内存 profile 对照热点。这样可以区分“解析逻辑本身占 CPU”“短命对象太多”和“结果对象一直存活”这三种完全不同的情况。
官方地址:https://pkg.go.dev/encoding/json
先用固定输入跑go test -bench . -benchmem,再分别生成 CPU 和内存 profile。只有当ns/op、B/op、allocs/op与 profile 指向同一类热点时,才开始改 JSON 代码。
先把 Marshal 和 Unmarshal 的输入固定下来
基准最容易犯的错误,是在循环里创建结构体、拼接字符串或生成随机数据。那样测到的是“准备输入 + JSON 编解码”的总成本,无法知道慢点到底在哪里。下面把两条路径拆开,编码只接收已经准备好的结构体,解码只读取已经准备好的 JSON 字节。
package jsonbench
import (
"encoding/json"
"testing"
)
type Order struct {
ID int64 `json:"id"`
Customer string `json:"customer"`
Items []string `json:"items"`
Paid bool `json:"paid"`
}
var (
sampleOrder = Order{
ID: 1001,
Customer: "demo",
Items: []string{"book", "cable", "adapter"},
Paid: true,
}
sampleJSON = []byte(`{"id":1001,"customer":"demo","items":["book","cable","adapter"],"paid":true}`)
sinkBytes []byte
sinkOrder Order
)
func BenchmarkMarshal(b *testing.B) {
b.ReportAllocs()
b.ResetTimer() // 排除测试数据构造和计时器初始化成本
for i := 0; i
这里的全局结果变量只是防止编译器消除调用,不代表生产代码应该使用全局状态。编码和解码要分开命名,否则后面看 profile 时很难把热点对应回具体路径。
用 benchmem 先区分时间和分配
运行基准时同时打开分配统计:
# 只重复基准,不把普通单元测试输出混进结果 go test -run '^$' -bench 'Benchmark(Marshal|Unmarshal)$' -benchmem -count=5
重点记录三列:ns/op 是单次耗时,B/op 是单次分配字节数,allocs/op 是单次分配次数。比较两个实现时,先保证输入、Go 版本、CPU 绑核方式和基准命令一致,不要拿一次运行的绝对数值跨机器下结论。
可以用下面的判断顺序:
ns/op明显变大,但B/op和allocs/op基本不变:优先查 JSON 结构复杂度、反射路径、字符串转义和业务层预处理。B/op或allocs/op明显上升,同时耗时也上升:优先查临时切片、重复转换、过大的中间结构和每次重新分配的缓冲区。- 分配数不高,但请求处理后堆仍持续增长:不要把它归因于单次编解码,继续用内存 profile 查谁持有了解码结果或缓存引用。

用 CPU profile 确认到底是谁在耗时
当 ns/op 指向 CPU 方向时,先生成一个足够长的 CPU profile。单次 profile 太短,采样点很少,容易把偶然函数误判成热点。
# 让基准运行一段固定时间,并保存 CPU 采样 go test -run '^$' -bench BenchmarkMarshal -benchtime=20s -cpuprofile=cpu.out # 查看累计采样最多的函数,再按调用关系展开 go tool pprof -top jsonbench.test cpu.out go tool pprof -http=:0 jsonbench.test cpu.out
先看 flat 和 cum:前者表示样本直接落在函数自身,后者包含它调用的下层函数。若热点集中在编码器内部,才值得比较字段数量、字符串转义和自定义 MarshalJSON;若热点落在业务层的对象转换或日志拼接,优化 JSON 调用本身通常收益很小。
解码也要单独采样:
# 解码路径使用独立 profile,避免和编码热点混在一起 go test -run '^$' -bench BenchmarkUnmarshal -benchtime=20s -cpuprofile=decode-cpu.out go tool pprof -top jsonbench.test decode-cpu.out
用内存 profile 判断是分配多还是活得久
内存 profile 至少要看两种口径:累计分配空间适合回答“谁制造了很多短命对象”,存活对象适合回答“谁让堆一直下不来”。同一个函数在两种口径下排名不同,并不矛盾。
# 采集编码路径的内存样本;输入和基准入口保持不变 go test -run '^$' -bench BenchmarkMarshal -benchtime=20s -memprofile=mem.out # 用累计分配空间观察短命对象制造者 go tool pprof -top -sample_index=alloc_space jsonbench.test mem.out # 用累计对象数观察分配次数密集的调用者 go tool pprof -top -sample_index=alloc_objects jsonbench.test mem.out # 对照仍存活的堆对象;它回答的是保留关系,不是总分配量 go tool pprof -top -sample_index=inuse_space jsonbench.test mem.out
如果 alloc_space 很高而 inuse_space 不高,常见方向是短命中间对象或重复缓冲;如果两者都高,则要检查结果对象、缓存、队列或闭包是否长期持有引用。不要仅凭函数名判断泄漏,profile 只能告诉你采样中的分配与存活关系,真正的生命周期还要回到代码的所有权。

根据证据选择优化动作
确认问题后再改实现,顺序通常是先减少无效工作,再减少重复分配:
- 先裁剪不需要输出的字段,避免为了传输或日志编码完整领域对象。
- 把重复的格式化、字符串拼接和临时转换移出热点循环;不要为了“少一次 Marshal”把错误处理变得不可见。
- 确实需要复用缓冲区时,再评估
bytes.Buffer或json.Encoder的生命周期,并保证复用边界不会把旧数据带到下一次请求。 - 如果热点来自自定义
MarshalJSON,先用基准证明它比默认路径更快且语义一致,再考虑维护成本。 - 优化后重新跑同一组 benchmark 和 profile;只改变输入规模或机器环境,就不能把前后结果直接相减。
最后记住:ns/op 是“花了多少时间”,B/op 和 allocs/op 是“制造了多少分配”,inuse_space 是“现在还留着多少”。三组数据一起指向同一热点时,优化才有可靠方向。
常见问题
为什么 benchmark 的 B/op 很高,但 CPU profile 不在 encoding/json?
可能是输入构造、转换层或日志路径分配更多。先把准备输入移出计时区,再分别给业务转换函数和 JSON 函数建 benchmark。
只看 allocs/op 能判断内存泄漏吗?
不能。allocs/op 只描述单次操作的分配次数,不能说明对象是否被长期引用。要结合 inuse_space、对象持有关系和请求生命周期继续定位。
-
343 收藏
-
424 收藏
-
212 收藏
-
208 收藏
-
408 收藏
-
205 收藏
-
443 收藏
-
437 收藏
-
Golang · Go问答 | 50分钟前 | 标准库 · 错误处理 · Go问答 · Go encoding/hex DecodeString InvalidByteError ErrLength 十六进制解码282 收藏
-
246 收藏
-
350 收藏
-
289 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习