登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

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 对象”当成深度增加点,在真正读取其成员前比较 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删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>