2017-07-15 67 views
1

我正在和json.Unmarshal一起工作,遇到了以下的怪癖。當運行下面的代碼,我得到的錯誤json: Unmarshal(non-pointer map[string]string)爲什麼json.Unmarshal需要一個指向地圖的指針,如果一個地圖是一個引用類型?

func main() { 
    m := make(map[string]string) 
    data := `{"foo": "bar"}` 
    err := json.Unmarshal([]byte(data), m) 
    if err != nil { 
     log.Fatal(err) 
    } 

    fmt.Println(m) 
} 

Playground

縱觀documentationjson.Unmarshal,也似乎沒有跡象表明指針是必需的。我能找到最接近的是下面的行

解組解析JSON編碼的數據,並將結果存儲在值用V指向。

關於解組遵循用於映射協議的線同樣不清楚,因爲它沒有提到指針。

要將JSON對象解組映射到映射,Unmarshal首先會建立要使用的映射。如果地圖爲零,Unmarshal會分配一個新地圖。否則Unmarshal將重新使用現有的地圖,保留現有的條目。 Unmarshal然後將來自JSON對象的鍵值對存儲到地圖中。映射的鍵類型必須是一個字符串,一個整數,或實現encoding.TextUnmarshaler。

爲什麼我必須傳遞一個指向json.Unmarshal的指針,特別是如果maps已經是引用類型?我知道,如果我將地圖傳遞給一個函數,並將數據添加到地圖中,則地圖的基礎數據將發生更改(請參閱the following playground example),這意味着將指針傳遞給地圖應該無關緊要。有人可以清除它嗎?

回答

5

如文檔中表示:

解組使用了元帥使用,分配映射的編碼的逆,切片和足尖rs必要時與......

Unmarshal可以分配變量(地圖,切片等)。如果我們將map而不是指針傳遞給map,則新分配的map對調用者不可見。下面的實施例(Go Playground)顯示這一點:

package main 

import (
    "fmt" 
) 

func mapFunc(m map[string]interface{}) { 
    m = make(map[string]interface{}) 
    m["abc"] = "123" 
} 

func mapPtrFunc(mp *map[string]interface{}) { 
    m := make(map[string]interface{}) 
    m["abc"] = "123" 

    *mp = m 
} 

func main() { 
    var m1, m2 map[string]interface{} 
    mapFunc(m1) 
    mapPtrFunc(&m2) 

    fmt.Printf("%+v, %+v\n", m1, m2) 
} 

在其中輸出是:

map[], map[abc:123] 

如果要求說,一個功能/方法可以分配必要時的可變和新分配的變量需要(a)變量必須在函數的返回語句(b)變量可以被分配給函數/方法參數。由於在go所有是通過值,在(b)的情況下,參數必須是指針。下圖說明在上述例子中發生什麼:

Illustration of variable allocation

  1. 起初,兩個地圖m1m2指向nil
  2. 調用mapFunc會將m1指向的值複製到m導致m也會指向nil圖。
  3. 如果在(1)地圖已經分配,​​則在(2)基礎地圖數據結構的由m1不是m1地址)所指向的地址將被複制到m。在這種情況下,m1m指向相同的地圖數據結構,因此通過m1修改地圖項也將是可見m
  4. mapFunc函數中,新地圖被分配並分配給m。沒有辦法將它分配給m1

在指針的情況下:

  1. 當調用mapPtrFunc,的m2地址將被複制到mp
  2. mapPtrFunc中,新地圖被分配並分配給*mp(而不是mp)。由於mp是指向m2的指針,因此將新映射分配到*mp將更改m2指向的值。請注意,mp的值不變,即地址爲m2
+0

我很喜歡這個答案。謝謝! – ollien

1

文檔的另一關鍵部分是這樣的:

來解組JSON成一個指針,解組第一處理的 的情況下的JSON作爲字面JSON空。在這種情況下,Unmarshal將 指針設置爲零。否則,Unmarshal會將JSON解組爲指針指向的值 。如果指針爲零,Unmarshal 會爲其指定一個新值。

如果解組接受了地圖,那就要離開地圖在相同狀態下的JSON是否null{}。但是通過使用指針,現在指針被設置爲nil並且它指向空映射。

注意,爲了讓解組能夠「指針設置爲無」,你實際上需要在一個指針傳遞到您的地圖指針:

package main 

import (
    "encoding/json" 
    "fmt" 
    "log" 
) 

func main() { 
    var m *map[string]string 
    data := `{}` 
    err := json.Unmarshal([]byte(data), &m) 
    if err != nil { 
     log.Fatal(err) 
    } 
    fmt.Println(m) 

    data = `null` 
    err = json.Unmarshal([]byte(data), &m) 
    if err != nil { 
     log.Fatal(err) 
    } 
    fmt.Println(m) 

    data = `{"foo": "bar"}` 
    err = json.Unmarshal([]byte(data), &m) 
    if err != nil { 
     log.Fatal(err) 
    } 
    fmt.Println(m) 
} 

此輸出:

&map[] 
<nil> 
&map[foo:bar] 
+0

是的,但是讓我困惑的是文檔的一部分是它引用了指針是否被傳遞了,但是它並沒有解釋使用指向地圖的指針的必要性。關於你的解釋,文檔中的下面幾行不會涵蓋你的情況,重新討論?「通過將Go值設置爲零,JSON null值解組成接口,映射,指針或片。因爲JSON中常常使用null來表示「不存在」,所以將JSON null解組爲任何其他Go類型對該值沒有影響,並且不會產生錯誤。「 – ollien

+0

這似乎並不表示不需要指針?特別是語言「切片,地圖,或指針」對不起,我可能會丟失一些東西 – ollien

+0

這就是所指的東西所指向的任何指針,你以'v'的形式傳入Unmarshal文檔的第一行是「Unmarshal解析JSON編碼的數據並將結果存儲在v指向的值中。「v」必須是一個指針,源代碼也非常清楚(https://golang.org/src/encoding/ json/decode.go#L177)你可以傳入一個指向接口的指針,一個指向地圖的指針,一個指向指針的指針(就像我在我的代碼中所做的那樣)等等。 –

1

你的觀點與「一片不過是一個指針」沒有什麼不同。切片(和地圖)使用指針來使它們輕量化,是的,但還有更多的事情可以使它們工作。例如,切片包含有關其長度和容量的信息。

至於爲什麼會出現這種情況,從代碼的角度來看,最後一行json.Unmarshal調用d.unmarshal(),它執行lines 176-179 of decode.go中的代碼。它基本上說「如果該值不是指針,或者是nil,則返回InvalidUnmarshalError」。

該文檔或許可以爲大約事情更清晰,但考慮幾件事情:

  1. 將如何JSON null值被分配到地圖爲nil,如果你不將指針傳遞到地圖?如果您需要修改地圖本身(而不是地圖中的項目)的能力,那麼將指針傳遞給需要修改的項目是有意義的。在這種情況下,這是地圖。
  2. 或者,假設您通過nil地圖到json.Unmarshal。在代碼json.Unmarshal使用最終調用等效make(map[string]string)後,值將根據需要解組。然而,你的函數中仍然有一個nil地圖,因爲你的地圖沒有指向任何東西。除了傳遞指向地圖的指針之外,沒有辦法解決這個問題。

但是,假設沒有必要傳遞地圖的地址,因爲「它已經是一個指針」,並且您已經初始化地圖,所以它不是nil。然後會發生什麼?嗯,如果我繞過我通過改變線路176讀取if rv.Kind() != reflect.Map && rv.Kind() != reflect.Ptr || rv.IsNil() {前面掛線測試,則可能發生這種情況:

`{"foo":"bar"}`: false map[foo:bar] 
`{}`: false map[] 
`null`: panic: reflect: reflect.Value.Set using unaddressable value [recovered] 
    panic: interface conversion: string is not error: missing method Error 

goroutine 1 [running]: 
json.(*decodeState).unmarshal.func1(0xc420039e70) 
    /home/kit/jstest/src/json/decode.go:172 +0x99 
panic(0x4b0a00, 0xc42000e410) 
    /usr/lib/go/src/runtime/panic.go:489 +0x2cf 
reflect.flag.mustBeAssignable(0x15) 
    /usr/lib/go/src/reflect/value.go:228 +0xf9 
reflect.Value.Set(0x4b8b00, 0xc420, 0x15, 0x4b8b00, 0x0, 0x15) 
    /usr/lib/go/src/reflect/value.go:1345 +0x2f 
json.(*decodeState).literalStore(0xc420084360, 0xc42000e3f8, 0x4, 0x8, 0x4b8b00, 0xc420, 0x15, 0xc420000100) 
    /home/kit/jstest/src/json/decode.go:883 +0x2797 
json.(*decodeState).literal(0xc420084360, 0x4b8b00, 0xc420, 0x15) 
    /home/kit/jstest/src/json/decode.go:799 +0xdf 
json.(*decodeState).value(0xc420084360, 0x4b8b00, 0xc420, 0x15) 
    /home/kit/jstest/src/json/decode.go:405 +0x32e 
json.(*decodeState).unmarshal(0xc420084360, 0x4b8b00, 0xc420, 0x0, 0x0) 
    /home/kit/jstest/src/json/decode.go:184 +0x224 
json.Unmarshal(0xc42000e3f8, 0x4, 0x8, 0x4b8b00, 0xc420, 0x8, 0x0) 
    /home/kit/jstest/src/json/decode.go:104 +0x148 
main.main() 
    /home/kit/jstest/src/jstest/main.go:16 +0x1af 

代碼導致該輸出:

package main 

// Note "json" is the local copy of the "encoding/json" source that I modified. 
import (
    "fmt" 
    "json" 
) 

func main() { 
    for _, data := range []string{ 
     `{"foo":"bar"}`, 
     `{}`, 
     `null`, 
    } { 
     m := make(map[string]string) 
     fmt.Printf("%#q: ", data) 
     if err := json.Unmarshal([]byte(data), m); err != nil { 
      fmt.Println(err) 
     } else { 
      fmt.Println(m == nil, m) 
     } 
    } 
} 

關鍵是這個這裏有點:

reflect.Value.Set using unaddressable value 

因爲你通過地圖的副本,它是不可尋址(即它已經從低層次的機器角度的臨時地址,甚至無地址)。我知道一個解決方法(x := new(Type)後跟*x = value,除了使用reflect包),但它實際上並沒有解決問題;你正在創建一個本地指針,無法返回給調用者並使用它來代替原始存儲位置!

所以現在嘗試指針:

 if err := json.Unmarshal([]byte(data), m); err != nil { 
      fmt.Println(err) 
     } else { 
      fmt.Println(m == nil, m) 
     } 

輸出:

`{"foo":"bar"}`: false map[foo:bar] 
`{}`: false map[] 
`null`: true map[] 

現在,它的工作原理。底線:使用指針如果對象本身可能會被修改(和文檔說它可能是,例如,如果null用於預期對象或數組(地圖或切片)的地方

+0

您能否澄清在這裏無法解決的概念?我寫了一個測試來觀察地圖副本的可尋址性的概念,我似乎能夠得到它的地址就好了。 https://play.golang.org/p/leUodSrTxG – ollien

+0

@ollien您正在創建一個指向該函數中'a'的指針,這意味着編譯器會使'a'可尋址。默認情況下,任何函數參數都是不可尋址的,包括方法接收器。如果您創建一個指向現有對象的指針,指針本身不可尋址,但指向的對象是可尋址的,因爲它具有地址。在https://play.golang.org/p/CR_3WSkm_V上查看使用反射的示例修改版本。 [reflect.Value]的''CanAddr'方法(https://golang.org/pkg/reflect/#Value.CanAddr)返回值是否可尋址。 –

+0

請注意,'reflect.ValueOf(foo)'導致返回'reflect.Value'的'CanAddr'方法返回'false'。同樣,'foo'按值傳遞給'reflect.ValueOf'。 'reflect.ValueOf(&foo)'將導致'&foo'仍然無法訪問,但是'foo'('reflect.Indirect(rv)'或'rv.Elem()')現在是可尋址的。 –

相關問題