首页 文章

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

提问于
浏览
2

我正在使用 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

查看documentationdocumentation,似乎没有迹象表明需要指针 . 我能找到的最接近的是以下一行

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

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

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

为什么我必须将指针传递给json.Unmarshal,特别是如果map已经是引用类型?我知道如果我将 Map 传递给一个函数,并将数据添加到 Map 中, Map 的基础数据将会更改(请参阅the following playground example),这意味着如果我将指针传递给 Map 则无关紧要 . 有人可以解决这个问题吗?

3 回答

  • 1

    如文档中所述:

    Unmarshal使用Marshal使用的编码的反转,根据需要分配 Map ,切片和指针,...

    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语句中 or (b)变量可以分配给函数/方法参数 . 因为在 go 中,一切都是按值传递的,在(b)的情况下,参数必须是指针 . 下图说明了上例中发生的情况:

    Illustration of variable allocation

    • 首先, Map m1m2 都指向 nil .

    • 调用 mapFunc 会将 m1 指向的值复制到 m ,结果 m 也会指向 nil map .

    • 如果在(1)中已经分配了映射,则在(2)中 m1 指向的基础 Map 数据结构的地址(不是 m1 的地址)将被复制到 m . 在这种情况下, m1m 都指向相同的 Map 数据结构,因此通过 m1 修改 Map 项目也将对 m 可见 .

    • mapFunc 函数中,分配了新映射并将其分配给 m . 无法将其分配给 m1 .

    如果是指针:

    • 调用 mapPtrFunc 时, m2 的地址将被复制到 mp .

    • mapPtrFunc 中,分配新 Map 并将其分配给 *mp (不是 mp ) . 由于 mp 是指向 m2 的指针,因此将新映射指定给 *mp 将更改 m2 指向的值 . 请注意, mp 的值保持不变,即 m2 的地址 .

  • 1

    文档的另一个关键部分是:

    要将JSON解组为指针,Unmarshal首先处理JSON的情况,即JSON文字为null . 在这种情况下,Unmarshal将指针设置为nil . 否则,Unmarshal将JSON解组为指针指向的值 . 如果指针为nil,则Unmarshal为其指定一个新值以指向它 .

    如果Unmarshall接受了 Map ,则无论JSON是 null 还是 {} ,都必须使 Map 保持相同状态 . 但是通过使用指针,现在指针设置为 nil 与指向空 Map 之间存在差异 .

    请注意,为了让Unmarshall能够“将指针设置为nil”,您实际上需要传入指向 Map 指针的指针:

    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]
    
  • 8

    你的观点与说“切片只不过是一个指针”没有什么不同 . 切片(和贴图)使用指针使它们变得轻量级,是的,但还有更多东西使它们起作用 . 例如,切片包含有关其长度和容量的信息 .

    至于为什么会发生这种情况,来自代码透视, json.Unmarshal 的最后一行调用 d.unmarshal() ,它执行lines 176-179 of decode.go中的代码 . 它基本上说“如果值不是指针,或者是 nil ,则返回 InvalidUnmarshalError ” .

    文档可能更清楚一些事情,但考虑一些事情:

    • 如果您没有't pass a pointer to the map? If you require the ability to modify the map itself (rather than the items in the map), then it makes sense to pass a pointer to the item that needs modified. In this case, it' Map ,JSON null 值如何分配给 Map nil .

    • 或者,假设您将 nil Map 传递给 json.Unmarshal . 在代码 json.Unmarshal 最终调用相当于 make(map[string]string) 之后,将根据需要对值进行解组 . 但是,您的函数中仍然有一个 nil Map ,因为您的 Map 没有指向任何内容 . 除了将指针传递给 Map 之外,没有办法解决这个问题 .

    但是,让's say there was no need to pass the address of your map because 2372972 , and you'已经初始化了 Map ,所以它不是 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
    

    因为您传递了 Map 的副本,所以它是无法寻址的(即它具有临时地址,甚至从低级机器角度看也没有地址) . 我知道一种方法( 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[]
    

    现在它有效 . 底线:如果对象本身可能被修改,则使用指针(并且文档说它可能是,例如,如果在期望对象或数组( Map 或切片)的地方使用 null ) .

相关问题