我正在与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
的{{3}},似乎没有迹象表明需要指针。我能找到的最接近的是
Unmarshal解析JSON编码的数据并将结果存储在v指向的值中。
关于协议Unmarshal的关于地图的线条同样不清楚,因为它没有引用指针。
要将JSON对象解组为地图,Unmarshal首先建立要使用的地图。如果地图为nil,则Unmarshal分配新地图。否则,Unmarshal会重用现有地图,保留现有条目。然后,Unmarshal将JSON对象中的键值对存储到地图中。映射的键类型必须是字符串,整数或实现encoding.TextUnmarshaler。
为什么我必须将指针传递给json.Unmarshal,特别是如果map已经是引用类型?我知道如果我将地图传递给一个函数,并将数据添加到地图中,地图的基础数据将会改变(参见documentation),这意味着如果我通过它就不重要了指向地图的指针。有人可以解决这个问题吗?
答案 0 :(得分:9)
如文件中所述:
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)变量必须在函数中<? em> return 语句或(b)可以将变量赋值给function / method参数。因为在go
中,所有都是按值传递的,所以在(b)的情况下,参数必须是指针。下图说明了上例中发生的情况:
m1
和m2
都指向nil
。mapFunc
会将m1
指向的值复制到m
,结果m
也会指向nil
地图。m1
指向的基础地图数据结构的地址(不是{{1}的地址}} )将被复制到m1
。在这种情况下,m
和m1
都指向相同的地图数据结构,因此通过m
修改地图项也将可见到m1
。 m
函数中,新地图已分配并分配给mapFunc
。无法将其分配给m
。 如果是指针:
m1
时,mapPtrFunc
的地址将被复制到m2
。 mp
中,新地图已分配并分配到mapPtrFunc
(不是*mp
)。由于mp
是指向mp
的指针,因此将新地图分配给m2
将更改*mp
指向的值。请注意,m2
的值不变,即mp
的地址。 答案 1 :(得分:1)
文档的另一个关键部分是:
要将JSON解组为指针,Unmarshal首先处理该情况 JSON是JSON文字null。在那种情况下,Unmarshal设置了 指向零的指针。否则,Unmarshal将JSON解组为 指针指向的值。如果指针为零,则为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]
答案 2 :(得分:1)
你的观点与说“切片只不过是一个指针”没有什么不同。切片(和贴图)使用指针使它们变得轻量级,是的,但还有更多东西使它们起作用。例如,切片包含有关其长度和容量的信息。
至于为什么会发生这种情况,从代码的角度来看,json.Unmarshal
的最后一行调用d.unmarshal()
,它执行lines 176-179 of decode.go中的代码。它基本上说“如果值不是指针,或者是nil
,则返回InvalidUnmarshalError
。”
文档可能对事情更清楚,但要考虑几件事情:
null
值如何作为nil
分配给地图?如果您需要能够修改地图本身(而不是地图中的项目),那么将指针传递给需要修改的项目是有意义的。在这种情况下,它就是地图。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, 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
。