[英]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)的情況下,參數必須是指針 。 下圖說明了上例中發生的情況:
m1
和m2
指向nil
。 mapFunc
會將m1
指向的值復制到m
結果m
也將指向nil
map。 m1
( 不是地址m1
)將被復制到m
。 在這種情況下, m1
和m
指向相同的地圖數據結構 ,因此通過m1
修改地圖項也將對m
可見 。 mapFunc
函數中,分配新映射並將其分配給m
。 無法將其分配給m1
。 如果是指針:
mapPtrFunc
, m2
的地址將被復制到mp
。 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
。
文檔可能更清楚一些事情,但考慮一些事情:
null
值如何分配給nil
? 如果您需要能夠修改地圖本身(而不是地圖中的項目),那么將指針傳遞給需要修改的項目是有意義的。 在這種情況下,它是地圖。 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.