encoding/gob和encoding/json均不支持直接序列化含循环引用的结构体,会panic或栈溢出;gob需实现BinaryMarshaler/BinaryUnmarshaler手动断开并重建引用,json则依赖jsoniter深度限制或自定义encoder,二者干预方式、成本及适用场景迥异。

encoding/gob 和 encoding/json 都不支持直接序列化含循环引用的结构体,一碰就 panic 或栈溢出;必须手动干预,但两者的干预方式、成本和适用场景完全不同。
gob 处理循环引用:必须实现 GobEncode/GobDecode 或 BinaryMarshaler/BinaryUnmarshaler
gob 默认走反射递归遍历,遇到 p → q → p 这类结构会无限展开,最终触发栈溢出。它不会报“循环引用”错误,而是直接 panic:runtime: goroutine stack exceeds 1000000000-byte limit。
解决路径只有一条:让类型自己接管序列化逻辑,断开指针链,用 ID 或索引替代引用,反序列化时再重建。
- 推荐用
BinaryMarshaler/BinaryUnmarshaler(更通用,也兼容其他二进制 encoder) -
GobEncode/GobDecode是 gob 专属接口,语义更窄,但行为一致 - 必须显式断开循环:比如把
*q字段暂存为int索引,或用 map 记录对象地址 → ID 映射 - 反序列化时不能直接赋值指针,要先构造所有对象,再批量修复引用关系(否则
unmarshalbinary中还拿不到目标实例)
json 处理循环引用:靠 jsoniter 深度限制 + 自定义 encoder,标准库无解
标准库 encoding/json 遇到循环引用直接 panic:json: unsupported value: encountered a cycle via ...,且无法拦截或降级处理。
jsoniter 是唯一可行的生产级替代:它通过 MaxDepth 截断嵌套,避免崩溃,但默认仍会重复展开相同对象(性能差),需配合自定义 encoder 才能真正跳过已访问对象。
立即学习“go语言免费学习笔记(深入)”;
- 默认
MaxDepth=1000,对深层嵌套有效,但对短循环(如 A→B→A)无效——它不检测地址重复,只数嵌套层数 - 要真正防循环,得自己实现
jsoniter.ValEncoder,用unsafe.Pointer做 visited set 缓存 - 注意:自定义 encoder 后,
jsoniter.Config.Froze()必须在所有注册完成之后调用,否则不生效 - 别依赖
json:",omitempty"消除循环字段——它只判零值,不判引用闭环
为什么不能靠注册类型或改字段名绕过循环问题?
这是常见误区。循环引用是数据图层面的问题,和类型注册、字段导出性、tag 标签完全无关。
-
gob.Register解决的是 interface{} 类型未知问题,不是循环问题 - 把
q *q改成Q *q(首字母大写)只解决“字段是否被编码”,不解决“编码时是否递归下去” - 加
json:"-"或gob:"-"虽然能跳过字段,但会丢失结构语义,等于放弃该引用关系 - 用
map[string]interface{}包一层?没用——循环仍在底层 struct 里,走到那一步照样 panic
实际选型关键点:你到底要什么?
如果目标是“保留完整对象图并可逆序列化”,gob + 自定义 marshaler 是目前 Go 生态里最可控的方案;如果只是想“不崩、能出 JSON 字符串”,jsoniter 的深度限制够用,但输出内容不可预测(可能截断中间节点)。
最容易被忽略的是重建引用的时机:gob 反序列化时,UnmarshalBinary 方法内无法安全解引用其他正在构建的对象,必须把所有对象先实例化进 slice/map,再用第二遍遍历修复指针——这点在神经网络节点、AST、DOM 树等场景中极易踩坑。


















