在递归生成json路径时所遇到的Slice append操作的问题

歆萌 2019-11-03

我们的需求是为根据json每一个value生成从rootkeypath

(为了方便说明我们暂时不考虑数组的情况,只考虑object/number/bool/string)

举个例子,对于以下json字符串

{
  "a": {
    "b":{
      "c":{
        "d0": "d0",
        "d1": "d1",
        "d2": "d2"
      }
    }
  }
}

我们希望最终生成以下形式

a.b.c.d0 = d0
a.b.c.d1 = d1
a.b.c.d2 = d2

为此我们我们定义了以下结构

type Entry struct{
  path []string
  val  interface{}
}

然后我们通过定义一个递归的函数来执行以下

func RecurseJson(jsonObj interface{}, path []string)([]*Entry, error){
    switch jsonObj.(type){
    case map[string]interface{}: //json object
        m := jsonObj.(map[string]interface{})
        var ret []*Entry
        for k, v := range m{
            newPath, err := RecurseJson(v, append(path, k)) // 递归
            if err != nil {
                return nil, err
            }
            ret = append(ret, newPath...)
        }
        return ret, nil

    default:
        return []*Entry{
            {
                path: path,
                val: jsonObj,
            },
        }, nil
    }
}

我们使用一个测试函数来测试执行结果

func TestRecurseJson(t *testing.T) {
    var obj interface{}
    err := json.Unmarshal([]byte(testJson), &obj)
    assert.NoError(t, err)

    ret, err := RecurseJson(obj, nil)
    assert.NoError(t, err)

    for _, entry := range ret{
        fmt.Printf("%v \t = %v\n", strings.Join(entry.path, "."), entry.val)
    }
}

输出的结果是

a.b.c.d2      = d0
a.b.c.d2      = d1
a.b.c.d2      = d2

而我们期望输出的结果是

a.b.c.d0      = d0
a.b.c.d1      = d1
a.b.c.d2      = d2

二者之间,第一行和第二行的path最末尾不一致,那么问题出在哪里呢?通过打点分析,发现在执行下面这一行的时候出现了问题

newPath, err := RecurseJson(v, append(path, k))

更细化一点,将他们拆分成两行

sub := append(path, k)
newPath, err := RecurseJson(v, sub)

打点发现在第一行,也就是append的地方会遇到问题,append之后会将我们上一次append的结果覆盖掉,那么为什么会这样呢?通过打点可以发现,当我们递归路径进入到c之后,也就是path["a", "b", "c"]时,cap(path)的值为4, len(path)的值为3,每一次append所返回的newPath,其实本质上是在切片中进行更改,newPathpath在底层指向的是同一片空间,只不过path的长度是3,而newPath长度是4,每一次append(path, k)之后,修改的都是内存空间中的同一个位置。画图举例

在递归生成json路径时所遇到的Slice append操作的问题

因此正确的操作是将newPath := append(path, k)改为

newPath := make([]string, len(path)
copy(newPath, path)
newPath := append(newPath, k)

需要分配给newPath一片新的空间,防止append在同一空间内不断更改。

相关推荐