json.decoder比json.unmarshal更适合大json流,因其基于io.reader边读边解析,内存稳定在kb级;而unmarshal需一次性加载全部json到内存,易触发oom。

为什么 json.Decoder 比 json.Unmarshal 更适合大JSON流
因为 json.Unmarshal 必须把整个 JSON 字符串读进内存再解析,遇到几百MB的 JSON 文件或长连接中的持续 JSON 流,直接 OOM。而 json.Decoder 基于 io.Reader,边读边解析,内存只保留当前处理的 token 和结构体字段,峰值内存通常压在几 KB 到几十 KB。
常见错误是拿 bytes.NewReader([]byte{...}) 包一个超大字节数组传给 json.NewDecoder——这等于没省内存,只是换了个 API 调用方式。真正要配的是带缓冲的流式源头,比如 os.File、net.Conn 或带 bufio.Reader 的管道。
- 对文件:直接
os.Open("huge.json")→bufio.NewReader(f)→json.NewDecoder(bufReader) - 对 HTTP 响应体:确保服务端返回的是
application/json且未启用 gzip(或手动解压后再塞给Decoder) - 避免反复调用
decoder.Decode(&v)解析单个顶层对象时传入指针为 nil 的变量,会 panic:panic: json: Unmarshal(nil *T)
如何安全解析嵌套深、字段多的 JSON 对象而不爆栈或卡死
深层嵌套(比如 500 层对象嵌套)会让默认的 json.Decoder 递归解析栈溢出;字段爆炸(如日志中带原始 HTML 片段或 base64 图片)会导致单次 Decode 卡住数秒甚至更久。解决方案不是调大栈,而是用 RawMessage + 按需解析。
例如,你只关心 data.items[].id 和 data.items[].status,其余字段体积大又无需结构化处理:
type Response struct {
Data struct {
Items []struct {
ID int `json:"id"`
Status string `json:"status"`
RawExt json.RawMessage `json:"ext"` // 不解析,留作字节切片
} `json:"items"`
} `json:"data"`
}
这样,每个 RawExt 只存引用(底层仍是原 JSON 字节的 subslice),不触发反序列化开销。后续若真需要解析某条 ext,再单独用 json.Unmarshal(item.RawExt, &extStruct)。
基于三引擎设计,从微信文章、新闻和博客网页提取干净内容,支持标题作者日期元数据,多格式和批量处理。
- 注意
json.RawMessage不能用于 map 的 value 类型(会 panic),只能用于 struct 字段或 slice 元素 - 如果字段名不固定(如动态 key),必须用
map[string]json.RawMessage,但要检查 key 是否合法,避免恶意构造的超长 key 耗尽内存 -
Decoder.DisallowUnknownFields()在流式场景下慎用——它会在遇到未知字段时立即报错,而很多真实 API 返回字段是渐进式增加的
怎么控制每秒解析条数并防止 goroutine 泄漏
流式 JSON 常见于 Kafka 消息、WebSocket 推送或 SSE,如果上游发得快、下游处理慢,不加流控就会积压 goroutine,最终耗尽内存或 fd。关键不是“并发多少”,而是“一次 Decode 多少、间隔多久”。
推荐用带缓冲的 channel 控制消费节奏,配合 time.Tick 或令牌桶(简单起见用 time.Sleep):
dec := json.NewDecoder(r)
for {
var item MyItem
if err := dec.Decode(&item); err == io.EOF {
break
} else if err != nil {
log.Printf("decode error: %v", err)
continue // 跳过坏数据,别 panic
}
select {
case outChan
- 永远用
err == io.EOF判断流结束,不要用err != nil统一处理——网络中断、编码错误、截断 JSON 都会返回其他 err,需区分对待 - 别在 for 循环里无条件启 goroutine 处理
item,除非你明确做了 worker pool 限流(比如用semaphore.NewWeighted(10)) - 如果 JSON 是数组格式(
[{},{},{}]),标准json.Decoder默认只解第一个对象。需先读[,再循环调用Decode,直到遇到];更稳妥的做法是用jsoniter或手动 tokenizer
为什么不用 jsoniter 或 go-json 就搞不定高性能场景
标准库 encoding/json 在小对象上够用,但一旦涉及百万级日志行解析、低延迟金融行情或 IoT 设备批量上报,它的反射开销和内存分配就成瓶颈。jsoniter 通过代码生成和 unsafe 操作跳过反射,go-json 则用 AST 预编译减少运行时判断。
但替换前必须确认两点:一是你的结构体字段是否稳定(go-json 要求编译期已知结构),二是是否允许使用非标准库(有些企业 CI 禁止第三方包)。实测对比(10MB JSON 数组,10 万条记录):
- 标准
json:约 1.2s,GC 压力明显,runtime.mallocgc占 CPU 30%+ -
jsoniter.ConfigCompatibleWithStandardLibrary:约 0.45s,内存分配减少 60% -
go-json(配合//go:generate go-json -type=MyItem):约 0.28s,零 alloc(如果字段全为基本类型)
不过,这些优化只有在你已经用对 Decoder、拆分 RawMessage、加好流控之后才值得投入——否则省下的那几十毫秒,早被一次无缓冲 channel 发送或没关的 HTTP body 吃掉了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










