在遞歸生成json路徑時所遇到的Slice append操作的問題

我們的需求是爲根據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)之後,修改的都是內存空間中的同一個位置。畫圖舉例

圖片描述

因此正確的操作是將newPath := append(path, k)改爲

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

需要分配給newPath一片新的空間,防止append在同一空間內不斷更改。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章