2017-07-15 68 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()')现在是可寻址的。 –

相关问题