Go json.Decoder逐个读取嵌套对象并限制深度的方法
来源:17golang原创
时间:2026-09-20 09:28:31 411浏览 收藏
处理来自接口或文件的 JSON 时,直接把整段数据 Decode 到 map[string]any 很快,但无法在读取过程中清楚地控制嵌套深度。更稳妥的做法是用 json.Decoder.Token 逐个读取对象边界:遇到对象开始就检查当前深度,遇到标量直接保存,遇到数组再递归处理其中的值。
官方文档:https://pkg.go.dev/encoding/json
核心规则是把“进入一个新的 JSON 对象”当成深度增加点,在真正读取其成员前比较depth与maxDepth。这样超深输入会尽早返回错误,而不是先构造一棵无法控制大小的对象树。
先划清 Decoder 的读取边界
Token 会返回 JSON 的分隔符、对象键或标量值;More 用来判断当前对象或数组是否还有成员。对象开始符 {、字段名、字段值和结束符 } 之间的边界明确后,递归函数只需要处理一类结构,不必把字符串切片或正则表达式当作 JSON 解析器。

下面的实现把“已读到对象开始符”和“需要先读一个值令牌”分开,便于在对象、数组和标量之间切换。代码中的注释只说明关键边界,完整 JSON 文本仍由 Decoder 负责解析。
package main
import (
"encoding/json"
"fmt"
"io"
)
// readObject 从对象开始符读取到结束符,并把当前层级传给子值。
func readObject(dec *json.Decoder, depth, maxDepth int) (map[string]any, error) {
if depth > maxDepth {
return nil, fmt.Errorf("JSON object depth %d exceeds limit %d", depth, maxDepth)
}
start, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("read object start: %w", err)
}
if start != json.Delim('{') {
return nil, fmt.Errorf("expected object start, got %v", start)
}
return readObjectBody(dec, depth, maxDepth)
}
// readObjectBody 假定 { 已被消费;字段值的对象会增加一层深度。
func readObjectBody(dec *json.Decoder, depth, maxDepth int) (map[string]any, error) {
result := make(map[string]any)
for dec.More() {
keyToken, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("read object key: %w", err)
}
key, ok := keyToken.(string)
if !ok {
return nil, fmt.Errorf("object key is %T", keyToken)
}
valueToken, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("read field %q: %w", key, err)
}
value, err := readValueAfterToken(dec, valueToken, depth, maxDepth)
if err != nil {
return nil, fmt.Errorf("field %q: %w", key, err)
}
result[key] = value
}
end, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("read object end: %w", err)
}
if end != json.Delim('}') {
return nil, fmt.Errorf("expected object end, got %v", end)
}
return result, nil
}
// readValueAfterToken 处理已经取出的值令牌,避免重复消费标量。
func readValueAfterToken(dec *json.Decoder, token json.Token, parentDepth, maxDepth int) (any, error) {
delim, isDelim := token.(json.Delim)
if !isDelim {
return token, nil // 字符串、数字、布尔值和 null 已经完整读取。
}
switch delim {
case '{':
return readObjectBody(dec, parentDepth+1, maxDepth)
case '[':
return readArrayBody(dec, parentDepth, maxDepth)
default:
return nil, fmt.Errorf("unexpected delimiter %q", delim)
}
}
// readArrayBody 逐个处理数组元素,数组本身不额外增加对象深度。
func readArrayBody(dec *json.Decoder, parentDepth, maxDepth int) ([]any, error) {
items := make([]any, 0)
for dec.More() {
token, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("read array value: %w", err)
}
value, err := readValueAfterToken(dec, token, parentDepth, maxDepth)
if err != nil {
return nil, err
}
items = append(items, value)
}
end, err := dec.Token()
if err != nil {
return nil, fmt.Errorf("read array end: %w", err)
}
if end != json.Delim(']') {
return nil, fmt.Errorf("expected array end, got %v", end)
}
return items, nil
}
func decodeLimited(data io.Reader, maxDepth int) (map[string]any, error) {
dec := json.NewDecoder(data)
value, err := readObject(dec, 0, maxDepth)
if err != nil {
return nil, err
}
// 顶层对象结束后检查 EOF,避免悄悄忽略第二段 JSON。
var extra json.Token
if err := dec.Decode(&extra); err != io.EOF {
if err == nil {
return nil, fmt.Errorf("trailing JSON value after root object")
}
return nil, fmt.Errorf("trailing data: %w", err)
}
return value, nil
}
用递归函数逐项读取嵌套对象
代码的关键不是递归本身,而是令牌消费责任固定:readObject 消费对象的开始与结束,readObjectBody 只消费成员,readValueAfterToken 根据已经取得的值令牌分派给对象、数组或标量分支。这样不会因为多调用一次 Token 而跳过字段。
如果只关心某几个字段,可以在 key 分支中只保留白名单,其余字段仍要消费完整值;不能直接跳过,否则下一个键会被误读成当前值。需要保留数字精度时,可在读取前调用 dec.UseNumber(),让数字以 json.Number 保存,而不是提前转成 float64。
在对象入口拒绝超深结构
本例把根对象记为深度 0。根对象的子对象进入 readObjectBody 时使用 parentDepth + 1,因此 maxDepth=2 允许根对象、第一层对象和第二层对象,进入第三层时立即报错。这个定义要写进接口约定,否则调用方很容易把“允许两层”理解成完全不同的层数。

深度限制只解决嵌套层数,不等于完整的资源保护。生产接口还应在 Decoder 外层使用带上限的读取器限制总字节数,并为数组长度、字符串长度和单次请求设置独立边界。错误信息可以保留字段路径,但不要把完整用户输入回显到日志。
数组、EOF 与生产边界怎么处理
标量令牌已经被 Token 消费,直接放进结果即可;数组开始符则交给 readArrayBody,数组中的对象仍会触发深度判断。语法不完整时,Token 或读取结束符会返回错误,应该向上包装字段名,方便定位输入位置。
根对象成功结束后再调用一次 Decode 检查 io.EOF,可以拒绝同一请求中拼接的第二个 JSON 值。若业务允许 JSON Lines,则应把这个检查改成明确的逐行协议,而不是把尾部内容默认为合法。
| 场景 | 建议 | 原因 |
|---|---|---|
| 对象嵌套过深 | 入口比较 depth 与 maxDepth | 尽早停止递归并保留边界 |
| 超大请求 | 外层限制读取字节数 | 深度小也可能包含巨量字段 |
| 数字精度敏感 | 使用 UseNumber | 避免默认浮点转换 |
| 尾部多余 JSON | 成功后检查 EOF | 避免静默接受拼接数据 |
常见问题
只读取第一层对象,还需要 Token 吗? 如果结构固定且体量可控,直接 Decode 到结构体更简单;需要动态字段、流式读取或深度防护时,再采用 Token 模式。
数组是否应该算一层深度? 没有唯一答案。本文把对象层数作为限制指标,数组只承载元素;如果业务把容器层级统一计数,应在数组分支显式增加计数,并同步修改接口文档和测试边界。
-
295 收藏
-
178 收藏
-
333 收藏
-
169 收藏
-
136 收藏
-
387 收藏
-
431 收藏
-
167 收藏
-
364 收藏
-
141 收藏
-
267 收藏
-
138 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习