我正在使用json.Unmarshal并遇到以下怪癖。 运行以下代码时,出现错误json: Unmarshal(non-pointer map[string]string)

func main() {
    m := make(map[string]string)
    data := `{"yunsang":"awei"}`
    err := json.Unmarshal([]byte(data), m)
    if err != nil {
        log.Fatal(err)
    }

    fmt.Println(m)
}

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

Unmarshal parses the JSON-encoded data and stores the result in the value pointed to by v.

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

To unmarshal a JSON object into a map, Unmarshal first establishes a map to 
use. If the map is nil, Unmarshal allocates a new map. Otherwise Unmarshal 
reuses the existing map, keeping existing entries. Unmarshal then stores key-
value pairs from the JSON object into the map. The map's key type must either 
be a string, an integer, or implement encoding.TextUnmarshaler.

为什么我必须传递一个指向json.Unmarshal的指针,尤其是在地图已经是引用类型的情况下? 我知道,如果我将一个映射传递给一个函数,然后向该映射添加数据,则该映射的基础数据将被更改,这意味着如果我将一个指针传递给 一张地图。 有人可以清理吗?

如文档中所述

Unmarshal uses the inverse of the encodings that Marshal uses, allocating maps, slices, 
and pointers as necessary, with ...

Unmarshal可以分配变量(映射,切片等)。如果我们传递map而不是指向map的指针,则新分配的map对调用者将不可见。

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
", m1, m2)
}

在这里插入图片描述
如果要求说函数/方法可以在必要时分配变量,并且新分配的变量需要对调用者可见,则解决方案将是:(a)变量必须在函数的return语句中;或者(b)变量可以分配给function / method参数。由于在go中,所有内容均按值传递,因此对于(b),参数必须是指针。下图说明了以上示例中发生的情况:

在这里插入图片描述
首先,映射m1和m2都指向nil。
调用mapFunc会将m1指向的值复制到m,结果m也将指向nil映射。
如果在(1)中已经分配了映射,则在(2)中m1所指向的基础映射数据结构的地址(不是m1的地址)将被复制到m。在这种情况下,m1和m都指向相同的地图数据结构,因此通过m1修改地图项也将对m可见。
在mapFunc函数中,新映射被分配并分配给m。无法将其分配给m1。
如果是指针:

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

您的观点与说"一个切片不过是一个指针"没什么不同。切片(和地图)使用指针使它们轻巧,是的,但是还有更多的东西可以使它们工作。例如,切片包含有关其长度和容量的信息。

至于为什么发生这种情况,从代码角度来看,json.Unmarshal的最后一行调用d.unmarshal(),该代码将执行decode.go的176-179行中的代码。它基本上说:“如果该值不是指针或为nil,则返回InvalidUnmarshalError。”

这些文档可能在内容上更为清晰,但请考虑以下几点:

如果不传递指向地图的指针,如何将JSON 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

因为您传递了地图的副本,所以它是无法寻址的(即从低级计算机的角度来看,它具有一个临时地址,甚至没有地址)。我知道解决此问题的一种方法(除了使用reflect包,在x := new(Type)之后是*x = value),但这实际上并不能解决问题。您正在创建一个不能返回给调用者的本地指针,并使用它代替了原始存储位置!

因此,现在尝试一个指针:

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,则使用指针)。

文档的另一个关键部分是

To unmarshal JSON into a pointer, Unmarshal first handles the case of
the JSON being the JSON literal null. In that case, Unmarshal sets the
pointer to nil. Otherwise, Unmarshal unmarshals the JSON into the
value pointed at by the pointer. If the pointer is nil, Unmarshal
allocates a new value for it to point to.

如果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)
}

在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐