簡體   English   中英

如果地圖是引用類型,為什么json.Unmarshal需要指向地圖的指針?

[英]Why does json.Unmarshal need a pointer to a map, if a map is a reference type?

我和json.Unmarshal一起工作,遇到了以下怪癖。 運行下面的代碼時,我得到錯誤json: Unmarshal(non-pointer map[string]string)

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)
}

操場

查看json.Unmarshal文檔 ,似乎沒有跡象表明需要指針。 我能找到的最接近的是以下一行

Unmarshal解析JSON編碼的數據並將結果存儲在v指向的值中。

關於協議Unmarshal的關於地圖的線條同樣不清楚,因為它沒有引用指針。

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

為什么我必須將指針傳遞給json.Unmarshal,特別是如果map已經是引用類型? 我知道如果我將地圖傳遞給一個函數,並將數據添加到地圖中,地圖的基礎數據將會被更改(請參閱下面的游樂場示例 ),這意味着如果我將指針傳遞給一張地圖。 有人可以解決這個問題嗎?

如文檔中所述:

Unmarshal使用Marshal使用的編碼的反轉,根據需要分配地圖,切片和指針,...

Unmarshal可以分配變量(map,slice等)。 如果我們通過一個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)變量必須在函數的return語句中 (b)變量可以分配給函數/方法參數。 因為在go 一切都是按值傳遞的,在(b)的情況下,參數必須是指針 下圖說明了上例中發生的情況:

變量分配的例證

  1. 首先,地圖m1m2指向nil
  2. 調用mapFunc會將m1指向的值復制到m結果m也將指向nil map。
  3. 如果在(1)地圖已經分配,則在(2)指向的基本地圖數據結構的地址m1不是地址m1 )將被復制到m 在這種情況下, m1m指向相同的地圖數據結構 ,因此通過m1修改地圖項也將對m 可見
  4. mapFunc函數中,分配新映射並將其分配給m 無法將其分配給m1

如果是指針:

  1. 調用mapPtrFuncm2的地址將被復制到mp
  2. mapPtrFunc ,新地圖被分配並分配給*mp (而不是mp )。 由於mp是指向m2指針,因此將新地圖指定給*mp將改變m2指向的值。 請注意, mp的值不變,即m2的地址。

文檔的另一個關鍵部分是:

要將JSON解組為指針,Unmarshal首先處理JSON的情況,即JSON文字為null。 在這種情況下,Unmarshal將指針設置為nil。 否則,Unmarshal將JSON解組為指針指向的值。 如果指針為nil,則Unmarshal為其指定一個新值以指向它。

如果Unmarshall接受了地圖,則無論JSON是null還是{} ,都必須使地圖保持相同狀態。 但是通過使用指針,現在指針設置為nil與指向空映射之間存在差異。

請注意,為了讓Unmarshall能夠“將指針設置為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]

你的觀點與說“切片只不過是一個指針”沒有什么不同。 切片(和貼圖)使用指針使它們變得輕量級,是的,但還有更多東西使它們起作用。 例如,切片包含有關其長度和容量的信息。

至於為什么會發生這種情況,從代碼的角度來看, json.Unmarshal的最后一行調用d.unmarshal() ,它執行decode.go的第176-179行中的代碼。 它基本上說“如果值不是指針,或者是nil ,則返回InvalidUnmarshalError

文檔可能更清楚一些事情,但考慮一些事情:

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

但是,假設沒有必要傳遞地圖的地址,因為“它已經是一個指針”了,你已經初始化了地圖,所以它不是nil 那么會發生什么? 好吧,如果我在前面鏈接的行中繞過測試,通過更改第176行來讀取if rv.Kind() != reflect.Map && rv.Kind() != reflect.Ptr || rv.IsNil() { 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, 0xc420012300, 0x15, 0x4b8b00, 0x0, 0x15)
    /usr/lib/go/src/reflect/value.go:1345 +0x2f
json.(*decodeState).literalStore(0xc420084360, 0xc42000e3f8, 0x4, 0x8, 0x4b8b00, 0xc420012300, 0x15, 0xc420000100)
    /home/kit/jstest/src/json/decode.go:883 +0x2797
json.(*decodeState).literal(0xc420084360, 0x4b8b00, 0xc420012300, 0x15)
    /home/kit/jstest/src/json/decode.go:799 +0xdf
json.(*decodeState).value(0xc420084360, 0x4b8b00, 0xc420012300, 0x15)
    /home/kit/jstest/src/json/decode.go:405 +0x32e
json.(*decodeState).unmarshal(0xc420084360, 0x4b8b00, 0xc420012300, 0x0, 0x0)
    /home/kit/jstest/src/json/decode.go:184 +0x224
json.Unmarshal(0xc42000e3f8, 0x4, 0x8, 0x4b8b00, 0xc420012300, 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 )。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM