likes
comments
collection
share

golang使用标准库json序列化无限递归问题

作者站长头像
站长
· 阅读数 13

本文是开发过程遇到的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

}

对应标准库中的部分:

golang使用标准库json序列化无限递归问题

带来的新问题是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。

  1. 首先检查当前类型(NodeWrapper)是否实现了MarshalJSON
  2. 当前类型未实现MarshalJSON,则检查当前类型的指针(*NodeWrapper)是否实现了MarshalJSON。
  3. 如果指针类型实现了MarshalJSON,用newCondAddrEncoder生成condAddrEncoder编码器
  4. 能取到指针,condAddrEncoder编码器优先用指针类型实现的MarshalJSON。
  5. 如果取不到指针,则condAddrEncoder用newTypeEncoder递归生成struct对应的编码器(此处是structEncoder,就是普通的struct的序列化)。

因为调用json.Marshal传递的是NodeWrapper值类型,不是指针,所以t.CanAddr肯定是false,所以实际肯定会走到structEncoder的逻辑

golang使用标准库json序列化无限递归问题

golang使用标准库json序列化无限递归问题

这样实现猜测是针对多重指针的,比如对**NodeWrapper序列化的时候,就会优先使用*NodeWrapper的MarshalJSON,否则就是用newPtrEncoder递归生成,最终保底是newStructEncoder

(w *NodeWrapper) MarshalJSON()方法实现,还有一个作用,是将NodeWrapper的method列表中的MarshalJSON删掉,具体需要再看下golang的结构体嵌套的时候方法的继承覆盖的逻辑

另外一个结论,就是有性能要求的场景下尽量不要使用MarshalJSON,会有额外的cpu和内存开销,因为这个函数只能用[]byte,在标准库里compat会做一次[]byte到byte.Buffer的解析和拷贝,执行时间会double。后续可以考虑用第三方json库优化性能

golang使用标准库json序列化无限递归问题

golang使用标准库json序列化无限递归问题

cpu对比:

golang使用标准库json序列化无限递归问题

内存对比:

golang使用标准库json序列化无限递归问题