当前位置:首页 > 文章列表 > Golang > Go教程 > Go json.Decoder逐个读取嵌套对象并限制深度的方法

Go json.Decoder逐个读取嵌套对象并限制深度的方法

来源:17golang原创 2026-09-20 09:28:31 0浏览 收藏

处理来自接口或文件的 JSON 时,直接把整段数据 Decode 到 map[string]any 很快,但无法在读取过程中清楚地控制嵌套深度。更稳妥的做法是用 json.Decoder.Token 逐个读取对象边界:遇到对象开始就检查当前深度,遇到标量直接保存,遇到数组再递归处理其中的值。

官方文档:https://pkg.go.dev/encoding/json

核心规则是把“进入一个新的 JSON 对象”当成深度增加点,在真正读取其成员前比较 depthmaxDepth。这样超深输入会尽早返回错误,而不是先构造一棵无法控制大小的对象树。

先划清 Decoder 的读取边界

Token 会返回 JSON 的分隔符、对象键或标量值;More 用来判断当前对象或数组是否还有成员。对象开始符 {、字段名、字段值和结束符 } 之间的边界明确后,递归函数只需要处理一类结构,不必把字符串切片或正则表达式当作 JSON 解析器。

JSON Token 与 Go Decoder 读取边界的静态说明图
图1:JSON Token 与 Go 读取边界的静态说明图,不是运行截图。

下面的实现把“已读到对象开始符”和“需要先读一个值令牌”分开,便于在对象、数组和标量之间切换。代码中的注释只说明关键边界,完整 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 允许根对象、第一层对象和第二层对象,进入第三层时立即报错。这个定义要写进接口约定,否则调用方很容易把“允许两层”理解成完全不同的层数。

Go json.Decoder 嵌套深度限制与错误边界的静态说明图
图2:嵌套深度限制与错误边界的静态说明图,不是运行截图。

深度限制只解决嵌套层数,不等于完整的资源保护。生产接口还应在 Decoder 外层使用带上限的读取器限制总字节数,并为数组长度、字符串长度和单次请求设置独立边界。错误信息可以保留字段路径,但不要把完整用户输入回显到日志。

数组、EOF 与生产边界怎么处理

标量令牌已经被 Token 消费,直接放进结果即可;数组开始符则交给 readArrayBody,数组中的对象仍会触发深度判断。语法不完整时,Token 或读取结束符会返回错误,应该向上包装字段名,方便定位输入位置。

根对象成功结束后再调用一次 Decode 检查 io.EOF,可以拒绝同一请求中拼接的第二个 JSON 值。若业务允许 JSON Lines,则应把这个检查改成明确的逐行协议,而不是把尾部内容默认为合法。

场景建议原因
对象嵌套过深入口比较 depth 与 maxDepth尽早停止递归并保留边界
超大请求外层限制读取字节数深度小也可能包含巨量字段
数字精度敏感使用 UseNumber避免默认浮点转换
尾部多余 JSON成功后检查 EOF避免静默接受拼接数据

常见问题

只读取第一层对象,还需要 Token 吗? 如果结构固定且体量可控,直接 Decode 到结构体更简单;需要动态字段、流式读取或深度防护时,再采用 Token 模式。

数组是否应该算一层深度? 没有唯一答案。本文把对象层数作为限制指标,数组只承载元素;如果业务把容器层级统一计数,应在数组分支显式增加计数,并同步修改接口文档和测试边界。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
RAG切分长文档时按标题层级保留上下文的策略RAG切分长文档时按标题层级保留上下文的策略
上一篇
RAG切分长文档时按标题层级保留上下文的策略
LibTV AI视频编辑入门:用镜头诊断表完成第一次返修
下一篇
LibTV AI视频编辑入门:用镜头诊断表完成第一次返修
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    130次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    198次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    143次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    122次使用
  • CMMLU中文大模型评估基准:功能、使用教程与应用场景解析
    CMMLU
    深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
    108次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码