golang使用标准库json序列化无限递归问题
本文是开发过程遇到的json递归问题的排查和解决过程,和一般的json递归不同的是,序列化的对象中有锁,会影响代码静态检测。
golang版本:1.13
需求是将多叉树序列化为json做持久化。
示例代码如下,因为多叉树节点中有父节点指针,json序列化会出现无限递归。
// Node 表示一个多叉树节点
type Node struct {
Parent *Node
Children map[string]*Node
sync.RWMutex
}
为了解决该问题,让*Node
类型实现MarshalJSON函数,在序列化的时候将Parent置空,序列化完成再恢复,同时为了避免嵌套调用,json.Marshal使用*t
func (t *Node) MarshalJSON() ([]byte, error) {
t.Lock()
p := t.Parent
// to avoid parent <-> children recursive call
t.Parent = nil
// use different type to avoid repeat call
ret, err := json.Marshal(*t)
t.Parent = p
t.Unlock()
return ret, err
}
对应标准库中的部分:
带来的新问题是go vet会检测到有copy lock,因为调用json.Marshal用了值传递,建议的方式是锁用指针
为了对原始代码改动最小,同时不影响以后Node结构体扩展增加字段,增加了一个NodeWrapper存储Node指针,但是这样仍然会有无限递归
/* to avoid lock to be copied by value */
type NodeWrapper struct {
*Node
}
func (t *Node) MarshalJSON() ([]byte, error) {
t.Lock()
p := t.Parent
// to avoid parent <-> children recursive call
t.Parent = nil
// use different type to avoid repeat call
w := NodeWrapper{
Node: t,
}
ret, err := json.Marshal(w)
t.Parent = p
t.Unlock()
return ret, err
}
神奇的是测试偶然发现只要让NodeWrapper指针实现MarshalJSON方法就可以规避。
这个函数不会被调用,但是可以避免无限递归,有点不可思议
// would not be called, but can prevent recursive call below
func (w *NodeWrapper) MarshalJSON() ([]byte, error) {
return json.Marshal(w)
}
单步调试发现,encoding/json的实现中,调用newTypeEncoder时参数allowAddr是true。
- 首先检查当前类型(NodeWrapper)是否实现了MarshalJSON
- 当前类型未实现MarshalJSON,则检查当前类型的指针(*NodeWrapper)是否实现了MarshalJSON。
- 如果指针类型实现了MarshalJSON,用newCondAddrEncoder生成condAddrEncoder编码器
- 能取到指针,condAddrEncoder编码器优先用指针类型实现的MarshalJSON。
- 如果取不到指针,则condAddrEncoder用newTypeEncoder递归生成struct对应的编码器(此处是structEncoder,就是普通的struct的序列化)。
因为调用json.Marshal传递的是NodeWrapper值类型,不是指针,所以t.CanAddr肯定是false,所以实际肯定会走到structEncoder的逻辑
这样实现猜测是针对多重指针的,比如对**NodeWrapper
序列化的时候,就会优先使用*NodeWrapper
的MarshalJSON,否则就是用newPtrEncoder递归生成,最终保底是newStructEncoder
(w *NodeWrapper) MarshalJSON()
方法实现,还有一个作用,是将NodeWrapper的method列表中的MarshalJSON删掉,具体需要再看下golang的结构体嵌套的时候方法的继承覆盖的逻辑
另外一个结论,就是有性能要求的场景下尽量不要使用MarshalJSON,会有额外的cpu和内存开销,因为这个函数只能用[]byte
,在标准库里compat会做一次[]byte
到byte.Buffer的解析和拷贝,执行时间会double。后续可以考虑用第三方json库优化性能
cpu对比:
内存对比:
转载自:https://juejin.cn/post/7153466931939377160