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。

操作指針類型變量時,一定要注意當前案例賦值傳值的問題!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章