Golang 操作 Map 错误记录(指针类型变量赋值问题)

一、错误代码示例

package main

import (
	"fmt"
	"time"
)

type TestUser struct {
	Name string
	Age  int
}

type TestData struct {
	Data map[string]interface{}
	Sort int
}

var TestChan chan *TestData

func init() {
	TestChan = make(chan *TestData, 100)
	Start()
}

func main() {
	arr := make([]*TestUser, 0)

	a := &TestUser{Name: "aaa", Age: 12}
	arr = append(arr, a)

	b := &TestUser{Name: "bbb", Age: 13}
	arr = append(arr, b)

	c := &TestUser{Name: "ccc", Age: 14}
	arr = append(arr, c)

	data := map[string]interface{}{"test": "指针Map容器"}
	for k, user := range arr {
		miniData := data
		miniData["user_name"] = user.Name
		miniData["user_age"] = user.Age
		d := &TestData{Data: miniData, Sort: k}
		fmt.Println("通道加入数据", d)
		TestChan <- d
	}

	time.Sleep(5 * time.Second)
}

func Start() {
	go func() {
		for {
			data := <-TestChan
			fmt.Println("通道取出数据", data)
		}
	}()
}

打印结果:
通道加入数据 &{map[descr:指针Map容器 user_age:12 user_name:aaa] 0}
通道加入数据 &{map[descr:指针Map容器 user_age:13 user_name:bbb] 1}
通道加入数据 &{map[descr:指针Map容器 user_age:14 user_name:ccc] 2}
通道取出数据 &{map[descr:指针Map容器 user_age:14 user_name:ccc] 0}
通道取出数据 &{map[descr:指针Map容器 user_age:14 user_name:ccc] 1}
通道取出数据 &{map[descr:指针Map容器 user_age:14 user_name:ccc] 2}

二、错误现象分析

上面的错误代码是按照实际遇到问题的业务代码逻辑写出来的,如果耐心看完上面的例子,应该能看出,加入 Channel 前打印的数据和从 Channel 取出来的数据不一致,而这里逻辑很简单,只是加入了一次 Channel,取出的数据就变了。

仔细观察,从 Channel 取出的数据,user_age 和 uer_name 都是循环最后一次遍历赋的值,为什么会这样?

三、错误原因说明

知识点:golang 中操作 map 、切片时,都是操作的变量对应的内存地址。

例子中的 data 参数类型为 map[string]interface{},将 data 赋值给 miniData 时,因为 golang 会操作对应的内存地址,这里就是直接把 data 变量的内存地址赋值给 miniData,所以操作 miniData 时,实际就是操作的 data。

所以在每次循环时,代码效果解释如下:

miniData := data                  
// data地址赋值给miniData 

miniData["user_name"] = user.Name 
// 等价于 data["user_name"] = user.Name 

miniData["user_age"] = user.Age
// 等价于 data["user_name"] = user.Name 

d := &TestData{Data: miniData, Sort: k}
// 等价于 d := &TestData{Data: data, Sort: k}

TestChan <- d
// 加入 Channel 中的数据,Data 值同样是最开始那个 data 的内存地址

取数据时:

data := <-TestChan

取数据的结果要看执行速度的情况;

如果上面到第二次循环时,此时 Channel 就取出一条数据,那对应打印的 Data 信息应该为循环第二次赋值的 data 数据值,即:&{map[descr:指针Map容器 user_age:13 user_name:bbb] 1};

当前案例打印结果很明显是 Channel 数据全部添加后,再从 Channel 依次取出数据,所以结果都为第三次循环修改的数据,user_age:14 user_name:ccc。

四、解决方案

知道错误原因,解决方案就很明了了,新建一个 Map 容器,把需要赋值的实际数据传入 Map,修改如下:

package main

import (
	"fmt"
	"time"
)

type TestUser struct {
	Name string
	Age  int
}

type TestData struct {
	Data map[string]interface{}
	Sort int
}

var TestChan chan *TestData

func init() {
	TestChan = make(chan *TestData, 100)
	Start()
}

func main() {
	arr := make([]*TestUser, 0)

	a := &TestUser{Name: "aaa", Age: 12}
	arr = append(arr, a)

	b := &TestUser{Name: "bbb", Age: 13}
	arr = append(arr, b)

	c := &TestUser{Name: "ccc", Age: 14}
	arr = append(arr, c)

	data := map[string]interface{}{"test": "指针Map容器"}
	for k, user := range arr {
		// ------------------修改---------------
		miniData := make(map[string]interface{}, 1)
		for k, v := range data {
			miniData[k] = v
		}
		// ------------------修改----------------
		miniData["user_name"] = user.Name
		miniData["user_age"] = user.Age

		d := &TestData{Data: miniData, Sort: k}
		fmt.Println("通道加入数据", d)
		TestChan <- d
	}

	time.Sleep(5 * time.Second)
}

func Start() {
	go func() {
		for {
			data := <-TestChan
			fmt.Println("通道取出数据", data)
		}
	}()
}


打印结果:
通道加入数据 &{map[test:指针Map容器 user_age:12 user_name:aaa] 0}
通道加入数据 &{map[test:指针Map容器 user_age:13 user_name:bbb] 1}
通道加入数据 &{map[test:指针Map容器 user_age:14 user_name:ccc] 2}
通道取出数据 &{map[test:指针Map容器 user_age:12 user_name:aaa] 0}
通道取出数据 &{map[test:指针Map容器 user_age:13 user_name:bbb] 1}
通道取出数据 &{map[test:指针Map容器 user_age:14 user_name:ccc] 2}

五、总结

golang 的变量类型可以分为 值类型 与 指针类型 两种:

  • 值类型 变量直接指向存在内存中的值,数据类型包括 int 系列、float 系列、bool、 string 、数组 和 结构体 struct;
  • 指针类型 变量指向的是内存地址,数据类型包括 指针、slice 切片、map、管道 chan、interface。

操作指针类型变量时,一定要注意当前案例赋值传值的问题!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值