原文鏈接:https://www.zhoubotong.site/post/60.html
之前寫過一篇文章,Go map定義的幾種方式以及修改技巧,今天發現還可以深入探討下開發中容易被忽視遺漏的問題,
以下以map爲例,演示大家日常開發中可能存在的問題。
Map的Value的賦值
我們來看下下面的代碼編譯會出現什麼結果?
package main
import "fmt"
type Person struct {
Name string
Sex int
}
func main() {
u := make(map[string]Person) // 定義一個map ,值爲上面Person的結構體
person := Person{"喬峯", 0}
u["p"] = person
u["p"].Sex = 18 // 按道理上面u["p"] 已經賦值成了person即Person{"喬峯", 0}的結構體,使用結構體.key=value應該沒毛病
fmt.Println(u["p"])
}
輸出:
./main.go:17:2: cannot assign to struct field u["p"].Sex in map
分析
爲啥不能使用u["p"].屬性賦值?map[string]Person 的value是一個Person結構值,所以當使用u["p"] = person,其實是一個值拷貝過程。
而u["p"]則是一個值引用(注意:所有像 int、float、bool 和 string 這些基本類型都屬於值類型,使用這些類型的變量是直接指向存在內存中的值。
Golang 中引用類型如指針,slice,map,channel,接口,函數等),再說簡單點就是:
[值類型]
包括:int、float、bool、string、數組、結構體
值類型變量聲明後,不管是否已經賦值,編譯器就會爲它分配內存,此時該值儲存在棧上,比如j=i 時候修改某變量i/j的值不會影響另一個。
[引用類型]
包括:指針、slice切片、map、chan、interface
變量直接存放的就是一個內存地址值,這個地址值指向的空間存的纔是值。所以修改器中一個,另外一個也會修改。
但是需要注意的是引用類型必須申請內存纔可以使用,make()是給引用類型申請內存空間。
既然u["p"]是值引用,那麼對u["p"].Sex = 18的修改當然是不允許滴了。
那要怎麼解決賦值問題?這裏我整理了兩種思路來解決:
解決方案一
package main
import "fmt"
type Person struct {
Name string
Sex int
}
func main() {
u := make(map[string]Person) // 定義一個map ,值爲上面Person的結構體
person := Person{"喬峯", 0}
u["p"] = person
//u["p"].Sex = 18 // 按道理上面u["p"] 已經賦值成了person即Person{"喬峯", 0}的結構體
//解決思路一
tmp := u["p"] //使用臨時變量將u["p"]即person 賦值給tmp,該操作爲值拷貝
tmp.Sex = 18 //tmp 具備struct
u["p"] = tmp //或者person =tmp 將tmp 賦值給u["p"]即person ,該操作爲值拷貝複製回去覆蓋
fmt.Println(u["p"])
}
以上輸出:
{喬峯 18}
上面tmp := u["p"]是先做一次值拷貝,做出一個tmp副本,然後修改該副本,然後再次發生了一次值拷貝複製回去:u["p"] = tmp,但是這種會在整個過程中發生2次結構體值拷貝,很明顯這周搞法性能很差。
自己覺的都看不下去了,有什麼更好的實現方案?接下來我們看下思路二:
package main
import "fmt"
type Person struct {
Name string
Sex int
}
func main() {
u := make(map[string]*Person) // 注意這裏的變化:定義一個map ,值爲上面Person的結構體指針
person := Person{"喬峯", 0}
u["p"] = &person
u["p"].Sex = 18
fmt.Println(u["p"])
}
上面我們將map的類型的value由Person值,改成Person指針。
這樣我們實際上每次修改的都是指針所指向的Person空間,指針本身是常指針,不能修改,只讀屬性,但是指向的Person是可以隨便修改的,
而且這裏我們看到並不需要值拷貝。只是一個指針的賦值。代碼明顯精簡了很多。
Map的遍歷賦值
我們來看下下面的代碼有什麼問題?能說明原因嗎?
package main
import "fmt"
type Person struct {
Name string
Sex int
}
func main() {
u := make(map[string]*Person) // 注意這裏的變化:定義一個map ,值爲上面Person的結構體指針
//定義Person數組
persons := []Person{
{Name: "喬峯", Sex: 18},
{Name: "鳩摩智", Sex: 23},
{Name: "慕容復", Sex: 22},
}
//將數組依次添加到map中
for _, person := range persons {
u[person.Name] = &person
}
//打印map
for k, v := range u {
fmt.Println(k, "=>", v.Name)
}
}
遍歷結果出現非預期的結果。仔細看,結果是錯誤的:
喬峯 => 慕容復
鳩摩智 => 慕容復
慕容復 => 慕容復
我們發現map中的3個key都指向數組中最後一個結構體。
分析
for循環中,person是結構體的一個拷貝副本,所以u
[person.Name]=&person
實際上一直都是指向了同一個指針, 最終該指針的值爲遍歷的最後一個struct的值拷貝
。
怎麼改?
package main
import "fmt"
type Person struct {
Name string
Sex int
}
func main() {
u := make(map[string]*Person)
//定義Person數組
persons := []Person{
{Name: "喬峯", Sex: 18},
{Name: "鳩摩智", Sex: 23},
{Name: "慕容復", Sex: 22},
}
// 遍歷結構體數組,依次賦值給map
for i := 0; i < len(persons); i++ {
u[persons[i].Name] = &persons[i]
}
//打印map
for k, v := range u {
fmt.Println(k, "=>", v.Name)
}
}
輸出:
鳩摩智 => 鳩摩智
慕容復 => 慕容復
喬峯 => 喬峯
通過上面的示例,希望能讓大家少走彎路,避免踩坑。