深入Go Map的使用技巧

原文鏈接: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)
    }
 }

輸出:

鳩摩智 => 鳩摩智
慕容復 => 慕容復
喬峯 => 喬峯

通過上面的示例,希望能讓大家少走彎路,避免踩坑。

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