如果地图是引用类型,为什么json.Unmarshal需要指向地图的指针?

时间:2017-07-15 20:33:08

标签: json go

我正在与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)
}

Playground

查看json.Unmarshal的{​​{3}},似乎没有迹象表明需要指针。我能找到的最接近的是

  

Unmarshal解析JSON编码的数据并将结果存储在v指向的值中。

关于协议Unmarshal的关于地图的线条同样不清楚,因为它没有引用指针。

  

要将JSON对象解组为地图,Unmarshal首先建立要使用的地图。如果地图为nil,则Unmarshal分配新地图。否则,Unmarshal会重用现有地图,保留现有条目。然后,Unmarshal将JSON对象中的键值对存储到地图中。映射的键类型必须是字符串,整数或实现encoding.TextUnmarshaler。

为什么我必须将指针传递给json.Unmarshal,特别是如果map已经是引用类型?我知道如果我将地图传递给一个函数,并将数据添加到地图中,地图的基础数据将会改变(参见documentation),这意味着如果我通过它就不重要了指向地图的指针。有人可以解决这个问题吗?

3 个答案:

答案 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)的情况下,参数必须是指针。下图说明了上例中发生的情况:

Illustration of variable allocation

  1. 首先,地图m1m2都指向nil
  2. 致电mapFunc会将m1指向的值复制到m,结果m也会指向nil地图。
  3. 如果在(1)中已经分配了地图,那么在(2)m1指向的基础地图数据结构的地址(不是{{1}的地址}} )将被复制到m1。在这种情况下,mm1都指向相同的地图数据结构,因此通过m修改地图项也将可见m1
  4. m函数中,新地图已分配并分配给mapFunc。无法将其分配给m
  5. 如果是指针:

    1. 致电m1时,mapPtrFunc的地址将被复制到m2
    2. 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。”

文档可能对事情更清楚,但要考虑几件事情:

  1. 如果您没有将指针传递给地图,JSON null值如何作为nil分配给地图?如果您需要能够修改地图本身(而不是地图中的项目),那么将指针传递给需要修改的项目是有意义的。在这种情况下,它就是地图。
  2. 或者,假设您将nil地图传递给json.Unmarshal。在代码json.Unmarshal最终调用相当于make(map[string]string)之后,将根据需要对值进行解组。但是,您的函数中仍然有nil映射,因为您的映射没有指向任何内容。除了将指针传递给地图之外,没有办法解决这个问题。
  3. 但是,假设没有必要传递地图的地址,因为“它已经是指针”,并且您已经初始化了地图,因此它不是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