【问题标题】:Why does json.Unmarshal need a pointer to a map, if a map is a reference type?如果映射是引用类型,为什么 json.Unmarshal 需要指向映射的指针?
【发布时间】:2017-12-20 16:58:57
【问题描述】:

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

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

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

关于 Unmarshal 遵循的 map 协议的行同样不清楚,因为它没有引用指针。

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

为什么我必须传递一个指向 json.Unmarshal 的指针,尤其是在映射已经是引用类型的情况下?我知道如果我将映射传递给函数,并将数据添加到映射,映射的基础数据将被更改(请参阅the following playground example),这意味着如果我将指针传递给地图。有人可以解决这个问题吗?

【问题讨论】:

    标签: json go


    【解决方案1】:

    如文档中所述:

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

    1. 首先,映射m1m2 都指向nil
    2. 调用mapFunc 会将m1 指向的值复制到m,结果m 也将指向nil 映射。
    3. 如果在 (1) 中映射已经分配,​​那么在 (2) 中 底层映射数据结构m1 指向的地址(不是m1 的地址 em>) 将被复制到m。在这种情况下m1m 都指向同一个地图数据结构,因此通过m1 修改地图项也将可见m
    4. mapFunc 函数中,新地图被分配并分配给m。无法将其分配给m1

    如果是指针:

    1. 调用mapPtrFunc时,m2的地址会被复制到mp
    2. mapPtrFunc 中,新映射被分配并分配给*mp(不是mp)。由于mp 是指向m2 的指针,因此将新映射分配给*mp 将更改m2 指向的值。注意mp的值不变,即m2的地址。

    【讨论】:

    • 我真的很喜欢这个答案。谢谢!
    【解决方案2】:

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

    为了将 JSON 解组为指针,Unmarshal 首先处理 JSON 是 JSON 文字 null。在这种情况下,Unmarshal 设置 指向零的指针。否则,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 null 值通过将 Go 值设置为 nil 来解组为接口、映射、指针或切片。因为 null 在 JSON 中经常用于表示“不存在”,所以将 JSON null 解组为任何其他 Go 类型都没有效果在值上并且不会产生错误。”
    • 这似乎不是表明不需要指针吗?特别是语言“切片、映射或指针”对不起,我可能遗漏了一些东西。
    • 指的是你传入的任何指针所指向的东西v。 Unmarshal 文档的第一行是“Unmarshal 解析 JSON 编码的数据并将结果存储在 v 指向的值中”。 v 必须是一个指针,并且来源也很清楚(golang.org/src/encoding/json/decode.go#L177)。您可以传入指向接口的指针、指向映射的指针、指向指针的指针(就像我在此处的代码中所做的那样)等等。
    • 你当然是对的,你可以创建一个改变地图的 Unmarshal 函数,但这更加一致,它允许将地图指针设置为nil,并解组为类似的东西整数 (var n int; data = "1"; json.Unmarshal([]byte(data), &amp;n))。通过让v 始终是一个指针,可能指向另一个指针,可能指向像映射这样的引用类型,可能指向像 int 这样的非引用类型,事情是一致的。
    • 无论如何,这就是他们选择这样做的方式,我个人认为文档足够清晰。但也许一句澄清会是一个有用的补充。
    【解决方案3】:

    您的观点与说“切片只不过是指针”没有什么不同。切片(和映射)使用指针使它们变得轻量,是的,但还有更多的东西可以使它们工作。例如,切片包含有关其长度和容量的信息。

    至于为什么会这样,从代码的角度来看,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 &amp;&amp; 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

    【讨论】:

    • 您能否在这里澄清不可寻址的概念?我写了一个测试来查看地图副本的可寻址性概念,我似乎能够很好地获得它的地址。 play.golang.org/p/leUodSrTxG
    • @ollien 您正在该函数中创建指向a 的指针,这意味着编译器使a 可寻址。默认情况下,任何函数参数都是不可寻址的,包括方法接收器。如果创建指向现有对象的指针,则指针本身是不可寻址的,但它指向的对象是可寻址的,因为它有一个地址。在 play.golang.org/p/CR_3WSkm_V 处查看使用反射的示例的修改版本。 CanAddr method of reflect.Value 返回值是否可寻址。
    • 请注意,reflect.ValueOf(foo) 会导致返回的reflect.ValueCanAddr 方法返回false。同样,foo 按值传递给 reflect.ValueOfreflect.ValueOf(&amp;foo) 将导致 &amp;foo 仍然无法寻址,但 fooreflect.Indirect(rv)rv.Elem())现在是可寻址的。
    • 我强烈建议您阅读 The Laws of Reflection 以帮助您了解反射的工作原理。
    猜你喜欢
    • 2017-08-08
    • 2011-09-26
    • 1970-01-01
    • 1970-01-01
    • 2017-04-26
    • 2015-07-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多