Go json.Decoder逐个读取嵌套对象并限制深度的方法
处理来自接口或文件的 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 模式。
数组是否应该算一层深度? 没有唯一答案。本文把对象层数作为限制指标,数组只承载元素;如果业务把容器层级统一计数,应在数组分支显式增加计数,并同步修改接口文档和测试边界。
RAG切分长文档时按标题层级保留上下文的策略
- 上一篇
- RAG切分长文档时按标题层级保留上下文的策略
- 下一篇
- LibTV AI视频编辑入门:用镜头诊断表完成第一次返修
-
- Golang · Go教程 | 9分钟前 |
- Go io.Pipe连接压缩器与上传器的背压处理方案
- 295浏览 收藏
-
- Golang · Go教程 | 18分钟前 | Go教程 · Go io.Copy限速 Go Reader节流 Go文件传输限速 io.Copy速率控制
- Go io.Copy接入限速Reader实现文件传输节流
- 178浏览 收藏
-
- Golang · Go教程 | 37分钟前 | Go教程 · 错误排查 · Go bufio.Scanner 超长日志行
- Go bufio.Scanner读取超长日志行的缓冲上限设置方式
- 333浏览 收藏
-
- Golang · Go教程 | 48分钟前 | go · csv · encoding/csv FieldsPerRecord ErrFieldCount
- Go encoding/csv处理可变列数文件的容错配置方法
- 169浏览 收藏
-
- Golang · Go教程 | 1小时前 |
- Go omitempty与指针字段组合表达JSON缺省值的设计要点
- 136浏览 收藏
-
- Golang · Go教程 | 1小时前 | JSON · go · encoding/json json.RawMessage
- Go json.RawMessage按字段类型分流的解析方案
- 387浏览 收藏
-
- Golang · Go教程 | 1小时前 | go · 迭代器 ·
- Go iter.Pull消费惰性迭代器后的停止与资源释放方案
- 167浏览 收藏
-
- Golang · Go教程 | 2小时前 | go · Go 浅拷贝 配置快照 maps.Clone map复制
- Go maps.Clone复制配置快照时的浅拷贝边界
- 141浏览 收藏
-
- Golang · Go教程 | 2小时前 |
- Go slices原地删除元素并避免底层数组泄漏的写法
- 138浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 130次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 198次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 143次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 122次使用
-
- CMMLU
- 深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
- 108次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- go语言中的defer关键字
- 2023-02-17 150浏览
-
- Golang中Interface接口的三个特性
- 2023-01-07 394浏览
-
- go语言中函数与方法介绍
- 2023-01-07 297浏览
-
- go语言数据类型之字符串string
- 2022-12-30 321浏览

