json: Unmarshal(non-pointer map[string]string) 问题

2,663 阅读5分钟

「这是我参与2022首次更文挑战的第10天,活动详情查看:2022首次更文挑战

首先说明 Go 参数传递都是值传递。

看个例子:

func alter_map(a map[int]int) {
	a[1] = 2
	fmt.Printf("%p\n", a) //查看修改【后】的map的内存地址
}

func TestChangeMap2(t *testing.T) {
	a := make(map[int]int, 2)
	a[2] = 1
	fmt.Printf("%p\n", &a) //查看修改【前】的map的内存地址
	alter_map(a)           //修改map

	fmt.Println(a)
}

运行结果:

=== RUN   TestChangeMap2
0xc00000e038
0xc000074360
map[1:2 2:1]
--- PASS: TestChangeMap2 (0.00s)
PASS

说明内存地址改变了,发现地址传递前后是完全不一样的,发生了数据拷贝。,但是为啥函数内部又可以修改 外部调用者的值呢? 虽然函数内部,函数调用者 是两个 map ,但是函数调用者调用函数时,会将函数外部的 map 存储结构的指针 拷贝到函数内部 map 的对应存储结构中,函数内部修改存储结构上的书,对外部也是可见的,因为执行的同一存储结构上的地址。

在看个例子:

func TestChangeMap3(t *testing.T) {
	a := make(map[int]int, 2)
	a[2] = 1
	fmt.Printf("%p\n", &a) //查看修改【前】的map的内存地址
	alterMap(&a)           //修改map

	fmt.Println(a)
}

func alterMap(a *map[int]int) {
	b := make(map[int]int)
	b[1] = 2
	*a = b
	fmt.Printf("%p\n", a) //查看修改【后】的map的内存地址
}

运行结果:

=== RUN   TestChangeMap3
0xc0000aa028
0xc0000aa028
map[1:2]
--- PASS: TestChangeMap3 (0.00s)
PASS

内存地址没有变,但是内容变了。

map 作为入参进行传递

当用make函数定义一个map时,make返回了一个Type类型,看似好像返回了一个值,而非指针,其实不然,其实还是通过指针进行传递都。

m := make(map[int]int)
func make(t Type, size ...IntegerType) Type

map 传递入参

func TestMapChuangti(t *testing.T) {
	m := make(map[int]int)
	m[1] = 1
	ChangeMap(m)
	fmt.Println(m)
}

//修改map
func ChangeMap(m map[int]int) {
	m[1] = 0
}

运行结果:

修改前: map[1:1]
修改后: map[1:0]

有个坑,如果把map修改为nil,则达不到修改map的目的。比如把ChangeMap函数改为:

func ChangeMap1(m map[int]int){
	m = nil
}

运行结果:

修改前: map[1:1]
修改后: map[1:1]

可以发现,map值并没有修改成nil。 结果:

  • `map·作为函数入参是作为指针进行传递的
  • 函数里面对map进行修改时,会同时修改源map的值,但是将map修改为nil时,则达不到预期效果。

json umarshal 问题

json: Unmarshal(non-pointer map[string]string)

看个例子:

func TestJsonUnmarshal_err(t *testing.T) {
	m := make(map[string]string)
	data := `{"foo": "bar"}`
	err := json.Unmarshal([]byte(data), m)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(m)
}

上述代码执行结果:

=== RUN   TestJsonUnmarshal_err
json: Unmarshal(non-pointer map[string]string)
map[]
--- PASS: TestJsonUnmarshal_err (0.00s)
PASS

执行结果中国,报错了一个 json: Unmarshal(non-pointer map[string]string)

func TestJsonUnmarshal_pass(t *testing.T) {
	m := make(map[string]string)
	data := `{"foo": "bar"}`
	err := json.Unmarshal([]byte(data), &m)
	if err != nil {
		fmt.Println(err)
	}

	fmt.Println(m)
}

执行结果:

=== RUN   TestJsonUnmarshal_pass
map[foo:bar]
--- PASS: TestJsonUnmarshal_pass (0.00s)
PASS

出现上述结果,为啥呢?

原因说明, Unmarshal 可以映射到变量上 map, slice, 如果我们传递一个变量 map, 2不是 指向 map , 然后新的映射的变量对调用者来说是不可以见的。

看个例子:

func mapFunc(m map[string]interface{}) {
	m = make(map[string]interface{}) //  如果注释掉会包 panic assignment to entry in nil map
	m["abc"] = "123"
}

func mapPtrFunc(mp *map[string]interface{}) {
	m := make(map[string]interface{})
	m["abc"] = "123"

	*mp = m
}

func TestJsonUnmarshal_example(t *testing.T) {
	var m1, m2 map[string]interface{}
	mapFunc(m1)
	mapPtrFunc(&m2)

	fmt.Printf("%+v, %+v\n", m1, m2)
}

运行结果:

=== RUN   TestJsonUnmarshal_example
map[], map[abc:123]
--- PASS: TestJsonUnmarshal_example (0.00s)
PASS

如果要一个局部变量对调用者可见, 一般需要满足下面两种情况:

  • a. 局部变量是函数的返回值
  • b. 局部变量赋值到函数/方法的入参中

go 中的一切都是值传递, 对于 b 这个情况,入参必须是一个指针。

上面的例子说明:

  1. m1m2 指向一个 nil
  2. 调用 mapFunc 会拷贝值指针 m1m , 因此 m 也是个 nil map
  3. 如果在步骤1 中 m1 已经初始化了,再步骤2 参数传递过程中存储m1 map 内容结构的地址(不是 m1 的地址)会拷贝到 m 中 这样的话, m1m 都指向相同的 map 数据结构,修改 m1 的值,会对 m 可见。
  4. 但是 在 mapFunc 中是是新建了一个 map. 这个不能对m1 可见

在这里插入图片描述

对于 mapPtrFunc 函数:

  1. 当调用 mapPtrFunc 时,是将 m2 的指针地址(不是存储数据结构的地址) 拷贝给 mp
  2. mapPtrFunc 中, 新建的 map m 赋值给 *mp(不是 mp), 也就是说指针 mp 指向的是 m2, 现在指针 mp 指向了新了的 m 也就指向了新的数据结构。
func TestChangeMap3(t *testing.T) {
	a := make(map[int]int, 2)
	a[2] = 1
	fmt.Printf("a %p\n", a) //查看修改【前】的map a 的内存地址
	fmt.Printf("&a %p\n", &a) //查看修改【前】的map a 的内存地址
	alterMap(&a)           //修改map
	fmt.Printf("a %p\n", a) //查看修改【后】的map a的内存地址
	fmt.Println(a)
}

func alterMap(a *map[int]int) {
	b := make(map[int]int)
	b[1] = 2
	fmt.Printf("b %p\n", b) //map b 的内存地址
	*a = b
	fmt.Printf("&a %p\n", a) //查看修改【后】的map a 指针的内存地址
}

执行结果:

=== RUN   TestChangeMap3
a 0xc000100300
&a 0xc00010e028
b 0xc000100330
&a 0xc00010e028
a 0xc000100330
map[1:2] // 原来的值 a[2] = 1 没了
--- PASS: TestChangeMap3 (0.00s)
PASS

关于指针 a, &a 的问题:

func main(){
	a := 10
	b := &a
	fmt.Printf("a:%d ptr:%p\n", a, &a)  //a:10 ptr:0xc00001a078
	fmt.Printf("b:%p type:%T\n", b, b)  //b:0xc00001a078 type:*int
	fmt.Println(&b)   // 0xc00000e018
}

在这里插入图片描述

参考资料