Go不支持尾递归优化,递归解析JSON易栈溢出;应使用json.RawMessage延迟解析并显式限制嵌套深度。

Go 里没有尾递归优化,别在 JSON 解析里写递归函数还指望它不爆栈。 你写的 UnmarshalJSON 方法哪怕看起来是“尾调用”,运行时照样一层层压栈,200 层嵌套就 panic,错误是 runtime: goroutine stack exceeds 1000000000-byte limit——这不是数据问题,是 Go 根本不优化这个。
为什么 tailFactorial(n-1, acc) 还是会栈溢出
因为 Go 编译器不支持尾调用优化(TCO)。tailFactorial 函数最后那句 return tailFactorial(n-1, n*acc) 看似“尾递归”,但 Go 不会复用栈帧。汇编层面它仍是普通函数调用,每次递归都新增栈帧。实测 10 万次调用直接 OOM。官方明确表示:不保证、不计划强制实现、不建议依赖。
- 栈空间默认仅 2KB,深度超过几百就危险
- 即使加了
if n > 50 { return errors.New("too deep") },也只是提前报错,没解决根本的栈增长模型 - 用
go f()启新 goroutine 模拟“尾调用”?每个 goroutine 至少 2KB 栈,10 万次就是 200MB 内存,还带调度开销和泄漏风险
json.RawMessage 是唯一靠谱的起点
把可能递归的字段声明为 Children json.RawMessage,不是 []Node,也不是 interface{}。它只是个 []byte 切片,不触发任何解码逻辑,零 CPU 开销,也不校验结构合法性。
- 适用于字段可能为
null、数组、对象,甚至嵌套自身(如树节点)的场景 - 赋值前必须确保字节有效:检查
len(data) > 0和json.Valid(data),否则后续Unmarshal会 panic - 后续真正需要子节点时,才对
Children调用json.Unmarshal,这时你可以按需选 struct、map 或继续用RawMessage - 别反复
make([]byte, len())拷贝;json.RawMessage可切片复用底层数组,前提是源 JSON 数据生命周期可控(比如来自io.Reader一次性读入的大 buffer)
自定义 UnmarshalJSON 必须防无限递归
如果真要结构化解析,得自己实现 UnmarshalJSON,但绝不能直接 json.Unmarshal(data, &n) ——这会触发无限递归。正确做法是用类型别名绕过方法重入:
立即学习“go语言免费学习笔记(深入)”;
func (n *Node) UnmarshalJSON(data []byte) error {
type Alias Node
aux := &struct {
Children json.RawMessage `json:"children"`
*Alias
}{
Alias: (*Alias)(n),
}
if err := json.Unmarshal(data, &aux); err != nil {
return err
}
// 此时 aux.Children 是原始字节,可安全解析子节点
if len(aux.Children) > 0 && json.Valid(aux.Children) {
var children []Node
if err := json.Unmarshal(aux.Children, &children); err != nil {
return err
}
n.Children = children
}
return nil
}
- 必须显式限制深度:在递归解析
children前加计数器,比如传入depth int参数,超过 50 直接返回错误 - 空字段或非法 JSON 片段会导致
json.Unmarshalpanic,所以务必先json.Valid - 别用
map[string]interface{}兜底:float64 会吞掉大整数 ID,空数组变 nil,时间字符串丢类型语义
真正难的不是写递归,是判断“哪一层该停、哪一层该转成流式跳过”。json.RawMessage 给你的是控制权,不是银弹;深度限制是防御线,不是性能优化。别在解析阶段做业务逻辑,先把结构稳住,再分层处理。


















