亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Go?復(fù)合類型之字典類型使用教程示例

 更新時(shí)間:2023年10月11日 09:14:31   作者:賈維斯Echo  
這篇文章主要為大家介紹了Go復(fù)合類型之字典類型使用教程示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

一、map類型介紹

1.1 什么是 map 類型?

map 是 Go 語(yǔ)言提供的一種抽象數(shù)據(jù)類型,它表示一組無(wú)序的鍵值對(duì)。用 key 和 value 分別代表 map 的鍵和值。而且,map 集合中每個(gè) key 都是唯一的:

和切片類似,作為復(fù)合類型的 map,它在 Go 中的類型表示也是由 key 類型與 value 類型組成的,就像下面代碼:

map[key_type]value_type

key 與 value 的類型可以相同,也可以不同:

map[string]string // key與value元素的類型相同
map[int]string    // key與value元素的類型不同

如果兩個(gè) map 類型的 key 元素類型相同,value 元素類型也相同,那么我們可以說(shuō)它們是同一個(gè) map 類型,否則就是不同的 map 類型。

這里,我們要注意,map 類型對(duì) value 的類型沒(méi)有限制,但是對(duì) key 的類型卻有嚴(yán)格要求,因?yàn)?map 類型要保證 key 的唯一性。因此在這里,你一定要注意:函數(shù)類型、map 類型自身,以及切片類型是不能作為 map 的 key 類型的。比如下面這段代碼:

// 函數(shù)類型不能作為key,因?yàn)楹瘮?shù)類型是不可比較的
func keyFunc() {}
m := make(map[string]int)
m[keyFunc] = 1 // 編譯錯(cuò)誤
// map類型不能作為key
m1 := make(map[string]int)
m[m1] = 1 // 編譯錯(cuò)誤
// 切片類型不能作為key,因?yàn)榍衅强勺冮L(zhǎng)度的,它們的內(nèi)容可能會(huì)在運(yùn)行時(shí)更改
s1 := []int{1,2,3}  
m[s1] = 1 // 編譯錯(cuò)誤

上面代碼中,試圖使用函數(shù)類型、map類型和切片類型作為key都會(huì)導(dǎo)致編譯錯(cuò)誤。

這是因?yàn)镚o語(yǔ)言在實(shí)現(xiàn)map時(shí),需要比較key是否相等,因此key需要支持==比較。但函數(shù)、map和切片類型的相等性比較涉及內(nèi)存地址,無(wú)法簡(jiǎn)單判斷,所以不能作為key。所以,key 的類型必須支持“==”和“!=”兩種比較操作符。

還需要注意的是,在 Go 語(yǔ)言中,函數(shù)類型、map 類型自身,以及切片只支持與 nil 的比較,而不支持同類型兩個(gè)變量的比較。如果像下面代碼這樣,進(jìn)行這些類型的比較,Go 編譯器將會(huì)報(bào)錯(cuò):

s1 := make([]int, 1)
s2 := make([]int, 2)
f1 := func() {}
f2 := func() {}
m1 := make(map[int]string)
m2 := make(map[int]string)
println(s1 == s2) // 錯(cuò)誤:invalid operation: s1 == s2 (slice can only be compared to nil)
println(f1 == f2) // 錯(cuò)誤:invalid operation: f1 == f2 (func can only be compared to nil)
println(m1 == m2) // 錯(cuò)誤:invalid operation: m1 == m2 (map can only be compared to nil)

1.2 map 類型特性

在Go中,map具有以下特性:

  • 無(wú)序性map中的鍵值對(duì)沒(méi)有固定的順序,遍歷時(shí)可能不按照添加的順序返回鍵值對(duì)。
  • 動(dòng)態(tài)增長(zhǎng)map是動(dòng)態(tài)的,它會(huì)根據(jù)需要自動(dòng)增長(zhǎng)以容納更多的鍵值對(duì),不需要預(yù)先指定大小。
  • 零值: 如果未初始化一個(gè)map,它將是nil,并且不能存儲(chǔ)鍵值對(duì)。需要使用make函數(shù)來(lái)初始化一個(gè)map。
  • 鍵的唯一性: 在同一個(gè)map中,每個(gè)鍵只能出現(xiàn)一次。如果嘗試使用相同的鍵插入多次,新值將覆蓋舊值。
  • 查詢效率高map的查詢操作通常非???,因?yàn)樗褂霉1韥?lái)存儲(chǔ)數(shù)據(jù),這使得通過(guò)鍵查找值的時(shí)間復(fù)雜度接近常數(shù)。
  • 引用類型map是一種引用類型,多個(gè)變量可以引用并共享同一個(gè)map實(shí)例。

二.map 變量的聲明和初始化

和切片一樣,為 map 類型變量顯式賦值有兩種方式:一種是使用復(fù)合字面值;另外一種是使用 make 這個(gè)預(yù)聲明的內(nèi)置函數(shù)。

2.1 方法一:使用 make 函數(shù)聲明和初始化(推薦)

這是最常見(jiàn)和推薦的方式,特別是在需要在map中添加鍵值對(duì)之前初始化map的情況下。使用make函數(shù)可以為map分配內(nèi)存并進(jìn)行初始化。

// 使用 make 函數(shù)聲明和初始化 map
myMap := make(map[keyType]valueType,capacity)

其中:

  • keyType 是鍵的類型。
  • valueType 是值的類型。
  • capacity表示map的初始容量,它是可選的,可以省略不寫。

例如:和切片通過(guò) make 進(jìn)行初始化一樣,通過(guò) make 的初始化方式,我們可以為 map 類型變量指定鍵值對(duì)的初始容量,但無(wú)法進(jìn)行具體的鍵值對(duì)賦值,就像下面代碼這樣:

// 創(chuàng)建一個(gè)存儲(chǔ)整數(shù)到字符串的映射
    m1 := make(map[int]string) // 未指定初始容量
    m1[1] = "key"
    fmt.Println(m1)

map 類型的容量不會(huì)受限于它的初始容量值,當(dāng)其中的鍵值對(duì)數(shù)量超過(guò)初始容量后,Go 運(yùn)行時(shí)會(huì)自動(dòng)增加 map 類型的容量,保證后續(xù)鍵值對(duì)的正常插入,比如下面這段代碼:

m2 := make(map[int]string, 2) // 指定初始容量為2
    m2[1] = "One"
    m2[2] = "Two"
    m2[3] = "Three"
    fmt.Println(m2) // 輸出:map[1:One 2:Two 3:Three] ,并不會(huì)報(bào)錯(cuò)
    fmt.Println(len(m2)) // 此時(shí),map容量已經(jīng)變?yōu)?

總結(jié):使用make函數(shù)初始化的map是空的,需要在后續(xù)代碼中添加鍵值對(duì)。

mm := make(map[int]string)
    fmt.Println(mm) // 輸出 map[]

2.2 方法二:使用復(fù)合字面值聲明初始化 map 類型變量

和切片類型變量一樣,如果我們沒(méi)有顯式地賦予 map 變量初值,map 類型變量的默認(rèn)值為 nil,比如,我們來(lái)看下面這段代碼:

var m map[string]int

if m == nil {
    fmt.Println("Map is nil")
} else {
    fmt.Println("Map is not nil")
}

不過(guò)切片變量和 map 變量在這里也有些不同。初值為零值 nil 的切片類型變量,可以借助內(nèi)置的 append 的函數(shù)進(jìn)行操作,這種在 Go 語(yǔ)言中被稱為“零值可用”。定義“零值可用”的類型,可以提升我們開(kāi)發(fā)者的使用體驗(yàn),我們不用再擔(dān)心變量的初始狀態(tài)是否有效。比如,創(chuàng)建一個(gè)存儲(chǔ)字符串到整數(shù)的映射,但 map 類型,因?yàn)樗鼉?nèi)部實(shí)現(xiàn)的復(fù)雜性,無(wú)法“零值可用”。所以,如果我們對(duì)處于零值狀態(tài)的 map 變量直接進(jìn)行操作,就會(huì)導(dǎo)致運(yùn)行時(shí)異常(panic),從而導(dǎo)致程序進(jìn)程異常退出:

var m map[string]int // m = nil
m["key"] = 1         // 發(fā)生運(yùn)行時(shí)異常:panic: assignment to entry in nil map

所以,我們必須對(duì) map 類型變量進(jìn)行顯式初始化后才能使用。我們先來(lái)看這句代碼:

m := map[int]string{}

這里,我們顯式初始化了 map 類型變量 m。不過(guò),你要注意,雖然此時(shí) map 類型變量 m 中沒(méi)有任何鍵值對(duì),但變量 m 也不等同于初值為 nil 的 map 變量。這個(gè)時(shí)候,我們對(duì) m 進(jìn)行鍵值對(duì)的插入操作,不會(huì)引發(fā)運(yùn)行時(shí)異常。

這里我們?cè)倏纯丛趺赐ㄟ^(guò)稍微復(fù)雜一些的復(fù)合字面值,對(duì) map 類型變量進(jìn)行初始化:

m1 := map[int][]string{
    1: []string{"val1_1", "val1_2"},
    3: []string{"val3_1", "val3_2", "val3_3"},
    7: []string{"val7_1"},
}

type Position struct { 
    x float64 
    y float64
}

m2 := map[Position]string{
    Position{29.935523, 52.568915}: "school",
    Position{25.352594, 113.304361}: "shopping-mall",
    Position{73.224455, 111.804306}: "hospital",
}

我們看到,上面代碼雖然完成了對(duì)兩個(gè) map 類型變量 m1 和 m2 的顯式初始化,但不知道你有沒(méi)有發(fā)現(xiàn)一個(gè)問(wèn)題,作為初值的字面值似乎有些“臃腫”。你看,作為初值的字面值采用了復(fù)合類型的元素類型,而且在編寫字面值時(shí)還帶上了各自的元素類型,比如作為 map[int] []string 值類型的[]string,以及作為 map[Position]string 的 key 類型的 Position。

別急!針對(duì)這種情況,Go 提供了“語(yǔ)法糖”。這種情況下,Go 允許省略字面值中的元素類型。因?yàn)?map 類型表示中包含了 key 和 value 的元素類型,Go 編譯器已經(jīng)有足夠的信息,來(lái)推導(dǎo)出字面值中各個(gè)值的類型了。我們以 m2 為例,這里的顯式初始化代碼和上面變量 m2 的初始化代碼是等價(jià)的:

m2 := map[Position]string{
    {29.935523, 52.568915}: "school",
    {25.352594, 113.304361}: "shopping-mall",
    {73.224455, 111.804306}: "hospital",
}

綜上,這種方式通常用于創(chuàng)建具有初始值的map。在這種情況下,不需要使用make函數(shù)。map的聲明方式如下:

// 使用字面量聲明和初始化 map
myMap := map[keyType]valueType{
    key1: value1,
    key2: value2,
    // ...
}

其中:

  • keyType 是鍵的類型
  • valueType 是值的類型
  • 然后使用大括號(hào) {} 包圍鍵值對(duì)

三.map 變量的傳遞開(kāi)銷(map是引用傳遞)

和切片類型一樣,map 也是引用類型。這就意味著 map 類型變量作為參數(shù)被傳遞給函數(shù)或方法的時(shí)候,實(shí)質(zhì)上傳遞的只是一個(gè)“描述符”,而不是整個(gè) map 的數(shù)據(jù)拷貝,所以這個(gè)傳遞的開(kāi)銷是固定的,而且也很小。

并且,當(dāng) map 變量被傳遞到函數(shù)或方法內(nèi)部后,我們?cè)诤瘮?shù)內(nèi)部對(duì) map 類型參數(shù)的修改在函數(shù)外部也是可見(jiàn)的。比如你從這個(gè)示例中就可以看到,函數(shù) foo 中對(duì) map 類型變量 m 進(jìn)行了修改,而這些修改在 foo 函數(shù)外也可見(jiàn)。

package main
import "fmt"
func foo(m map[string]int) {
    m["key1"] = 11
    m["key2"] = 12
}
func main() {
    m := map[string]int{
        "key1": 1,
        "key2": 2,
    }
    fmt.Println(m) // map[key1:1 key2:2]  
    foo(m)
    fmt.Println(m) // map[key1:11 key2:12] 
}

所以,map 引用類型。當(dāng) map 被賦值為一個(gè)新變量的時(shí)候,它們指向同一個(gè)內(nèi)部數(shù)據(jù)結(jié)構(gòu)。因此,當(dāng)改變其中一個(gè)變量,就會(huì)影響到另一變量。

四.map 的內(nèi)部實(shí)現(xiàn)

4.1 map 類型在 Go 運(yùn)行時(shí)層實(shí)現(xiàn)的示意圖

和切片相比,map 類型的內(nèi)部實(shí)現(xiàn)要更加復(fù)雜。Go 運(yùn)行時(shí)使用一張哈希表來(lái)實(shí)現(xiàn)抽象的 map 類型。運(yùn)行時(shí)實(shí)現(xiàn)了 map 類型操作的所有功能,包括查找、插入、刪除等。在編譯階段,Go 編譯器會(huì)將 Go 語(yǔ)法層面的 map 操作,重寫成運(yùn)行時(shí)對(duì)應(yīng)的函數(shù)調(diào)用。大致的對(duì)應(yīng)關(guān)系是這樣的:

// 創(chuàng)建map類型變量實(shí)例
m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m)
// 插入新鍵值對(duì)或給鍵重新賦值
m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") v是用于后續(xù)存儲(chǔ)value的空間的地址
// 獲取某鍵的值 
v := m["key"]      → v := runtime.mapaccess1(maptype, m, "key")
v, ok := m["key"]  → v, ok := runtime.mapaccess2(maptype, m, "key")
// 刪除某鍵
delete(m, "key")   → runtime.mapdelete(maptype, m, “key”)

這是 map 類型在 Go 運(yùn)行時(shí)層實(shí)現(xiàn)的示意圖:

我們可以看到,和切片的運(yùn)行時(shí)表示圖相比,map 的實(shí)現(xiàn)示意圖顯然要復(fù)雜得多。接下來(lái),我們結(jié)合這張圖來(lái)簡(jiǎn)要描述一下 map 在運(yùn)行時(shí)層的實(shí)現(xiàn)原理。接下來(lái)我們來(lái)看一下一個(gè) map 變量在初始狀態(tài)、進(jìn)行鍵值對(duì)操作后,以及在并發(fā)場(chǎng)景下的 Go 運(yùn)行時(shí)層的實(shí)現(xiàn)原理。

4.2 初始狀態(tài)

從圖中我們可以看到,與語(yǔ)法層面 map 類型變量(m)一一對(duì)應(yīng)的是 *runtime.hmap 的實(shí)例,即 runtime.hmap 類型的指針,也就是我們前面在講解 map 類型變量傳遞開(kāi)銷時(shí)提到的 map 類型的描述符。hmap 類型是 map 類型的頭部結(jié)構(gòu)(header),它存儲(chǔ)了后續(xù) map 類型操作所需的所有信息,包括:

真正用來(lái)存儲(chǔ)鍵值對(duì)數(shù)據(jù)的是桶,也就是 bucket,每個(gè) bucket 中存儲(chǔ)的是 Hash 值低 bit 位數(shù)值相同的元素,默認(rèn)的元素個(gè)數(shù)為 BUCKETSIZE(值為 8,Go 1.17 版本中在 $GOROOT/src/cmd/compile/internal/reflectdata/reflect.go 中定義,與 runtime/map.go 中常量 bucketCnt 保持一致)。

當(dāng)某個(gè) bucket(比如 buckets[0]) 的 8 個(gè)空槽 slot)都填滿了,且 map 尚未達(dá)到擴(kuò)容的條件的情況下,運(yùn)行時(shí)會(huì)建立 overflow bucket,并將這個(gè) overflow bucket 掛在上面 bucket(如 buckets[0])末尾的 overflow 指針上,這樣兩個(gè) buckets 形成了一個(gè)鏈表結(jié)構(gòu),直到下一次 map 擴(kuò)容之前,這個(gè)結(jié)構(gòu)都會(huì)一直存在。

從圖中我們可以看到,每個(gè) bucket 由三部分組成,從上到下分別是 tophash 區(qū)域、key 存儲(chǔ)區(qū)域和 value 存儲(chǔ)區(qū)域。

4.3 tophash 區(qū)域

當(dāng)我們向 map 插入一條數(shù)據(jù),或者是從 map 按 key 查詢數(shù)據(jù)的時(shí)候,運(yùn)行時(shí)都會(huì)使用哈希函數(shù)對(duì) key 做哈希運(yùn)算,并獲得一個(gè)哈希值(hashcode)。這個(gè) hashcode 非常關(guān)鍵,運(yùn)行時(shí)會(huì)把 hashcode“一分為二”來(lái)看待,其中低位區(qū)的值用于選定 bucket,高位區(qū)的值用于在某個(gè) bucket 中確定 key 的位置。我把這一過(guò)程整理成了下面這張示意圖,你理解起來(lái)可以更直觀:

因此,每個(gè) bucket 的 tophash 區(qū)域其實(shí)是用來(lái)快速定位 key 位置的,這樣就避免了逐個(gè) key 進(jìn)行比較這種代價(jià)較大的操作。尤其是當(dāng) key 是 size 較大的字符串類型時(shí),好處就更突出了。這是一種以空間換時(shí)間的思路。

4.4 key 存儲(chǔ)區(qū)域

接著,我們看 tophash 區(qū)域下面是一塊連續(xù)的內(nèi)存區(qū)域,存儲(chǔ)的是這個(gè) bucket 承載的所有 key 數(shù)據(jù)。運(yùn)行時(shí)在分配 bucket 的時(shí)候需要知道 key 的 Size。那么運(yùn)行時(shí)是如何知道 key 的 size 的呢?

當(dāng)我們聲明一個(gè) map 類型變量,比如 var m map[string]int 時(shí),Go 運(yùn)行時(shí)就會(huì)為這個(gè)變量對(duì)應(yīng)的特定 map 類型,生成一個(gè) runtime.maptype 實(shí)例。如果這個(gè)實(shí)例已經(jīng)存在,就會(huì)直接復(fù)用。maptype 實(shí)例的結(jié)構(gòu)是這樣的:

type maptype struct {
    typ        _type
    key        *_type
    elem       *_type
    bucket     *_type // internal type representing a hash bucket
    keysize    uint8  // size of key slot
    elemsize   uint8  // size of elem slot
    bucketsize uint16 // size of bucket
    flags      uint32
}

我們可以看到,這個(gè)實(shí)例包含了我們需要的 map 類型中的所有"元信息"。我們前面提到過(guò),編譯器會(huì)把語(yǔ)法層面的 map 操作重寫成運(yùn)行時(shí)對(duì)應(yīng)的函數(shù)調(diào)用,這些運(yùn)行時(shí)函數(shù)都有一個(gè)共同的特點(diǎn),那就是第一個(gè)參數(shù)都是 maptype 指針類型的參數(shù)。

Go 運(yùn)行時(shí)就是利用 maptype 參數(shù)中的信息確定 key 的類型和大小的。map 所用的 hash 函數(shù)也存放在 maptype.key.alg.hash(key, hmap.hash0) 中。同時(shí) maptype 的存在也讓 Go 中所有 map 類型都共享一套運(yùn)行時(shí) map 操作函數(shù),而不是像 C++ 那樣為每種 map 類型創(chuàng)建一套 map 操作函數(shù),這樣就節(jié)省了對(duì)最終二進(jìn)制文件空間的占用。

4.5 value 存儲(chǔ)區(qū)域

我們?cè)俳又?key 存儲(chǔ)區(qū)域下方的另外一塊連續(xù)的內(nèi)存區(qū)域,這個(gè)區(qū)域存儲(chǔ)的是 key 對(duì)應(yīng)的 value。和 key 一樣,這個(gè)區(qū)域的創(chuàng)建也是得到了 maptype 中信息的幫助。Go 運(yùn)行時(shí)采用了把 key 和 value 分開(kāi)存儲(chǔ)的方式,而不是采用一個(gè) kv 接著一個(gè) kv 的 kv 緊鄰方式存儲(chǔ),這帶來(lái)的其實(shí)是算法上的復(fù)雜性,但卻減少了因內(nèi)存對(duì)齊帶來(lái)的內(nèi)存浪費(fèi)。

我們以 map[int8]int64 為例,看看下面的存儲(chǔ)空間利用率對(duì)比圖:

你會(huì)看到,當(dāng)前 Go 運(yùn)行時(shí)使用的方案內(nèi)存利用效率很高,而 kv 緊鄰存儲(chǔ)的方案在 map[int8]int64 這樣的例子中內(nèi)存浪費(fèi)十分嚴(yán)重,它的內(nèi)存利用率是 72/128=56.25%,有近一半的空間都浪費(fèi)掉了。

另外,還有一點(diǎn)我要跟你強(qiáng)調(diào)一下,如果 key 或 value 的數(shù)據(jù)長(zhǎng)度大于一定數(shù)值,那么運(yùn)行時(shí)不會(huì)在 bucket 中直接存儲(chǔ)數(shù)據(jù),而是會(huì)存儲(chǔ) key 或 value 數(shù)據(jù)的指針。目前 Go 運(yùn)行時(shí)定義的最大 key 和 value 的長(zhǎng)度是這樣的:

// $GOROOT/src/runtime/map.go
const (
    maxKeySize  = 128
    maxElemSize = 128
)

五.map 擴(kuò)容

我們前面提到過(guò),map 會(huì)對(duì)底層使用的內(nèi)存進(jìn)行自動(dòng)管理。因此,在使用過(guò)程中,當(dāng)插入元素個(gè)數(shù)超出一定數(shù)值后,map 一定會(huì)存在自動(dòng)擴(kuò)容的問(wèn)題,也就是怎么擴(kuò)充 bucket 的數(shù)量,并重新在 bucket 間均衡分配數(shù)據(jù)的問(wèn)題。

那么 map 在什么情況下會(huì)進(jìn)行擴(kuò)容呢?Go 運(yùn)行時(shí)的 map 實(shí)現(xiàn)中引入了一個(gè) LoadFactor(負(fù)載因子),當(dāng) count > LoadFactor * 2^B 或 overflow bucket 過(guò)多時(shí),運(yùn)行時(shí)會(huì)自動(dòng)對(duì) map 進(jìn)行擴(kuò)容。目前 Go 1.17 版本 LoadFactor 設(shè)置為 6.5(loadFactorNum/loadFactorDen)。這里是 Go 中與 map 擴(kuò)容相關(guān)的部分源碼:

// $GOROOT/src/runtime/map.go
const (
  ... ...
  loadFactorNum = 13
  loadFactorDen = 2
  ... ...
)
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  ... ...
  if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
    hashGrow(t, h)
    goto again // Growing the table invalidates everything, so try again
  }
  ... ...
}

這兩方面原因?qū)е碌臄U(kuò)容,在運(yùn)行時(shí)的操作其實(shí)是不一樣的。如果是因?yàn)?overflow bucket 過(guò)多導(dǎo)致的“擴(kuò)容”,實(shí)際上運(yùn)行時(shí)會(huì)新建一個(gè)和現(xiàn)有規(guī)模一樣的 bucket 數(shù)組,然后在 assign 和 delete 時(shí)做排空和遷移。

如果是因?yàn)楫?dāng)前數(shù)據(jù)數(shù)量超出 LoadFactor 指定水位而進(jìn)行的擴(kuò)容,那么運(yùn)行時(shí)會(huì)建立一個(gè)兩倍于現(xiàn)有規(guī)模的 bucket 數(shù)組,但真正的排空和遷移工作也是在 assign 和 delete 時(shí)逐步進(jìn)行的。原 bucket 數(shù)組會(huì)掛在 hmap 的 oldbuckets 指針下面,直到原 buckets 數(shù)組中所有數(shù)據(jù)都遷移到新數(shù)組后,原 buckets 數(shù)組才會(huì)被釋放。你可以結(jié)合下面的 map 擴(kuò)容示意圖來(lái)理解這個(gè)過(guò)程,這會(huì)讓你理解得更深刻一些:

六.map 與并發(fā)

接著我們來(lái)看一下 map 和并發(fā)。從上面的實(shí)現(xiàn)原理來(lái)看,充當(dāng) map 描述符角色的 hmap 實(shí)例自身是有狀態(tài)的(hmap.flags),而且對(duì)狀態(tài)的讀寫是沒(méi)有并發(fā)保護(hù)的。所以說(shuō) map 實(shí)例不是并發(fā)寫安全的,也不支持并發(fā)讀寫。如果我們對(duì) map 實(shí)例進(jìn)行并發(fā)讀寫,程序運(yùn)行時(shí)就會(huì)拋出異常。你可以看看下面這個(gè)并發(fā)讀寫 map 的例子:

package main
import (
    "fmt"
    "time"
)
func doIteration(m map[int]int) {
    for k, v := range m {
        _ = fmt.Sprintf("[%d, %d] ", k, v)
    }
}
func doWrite(m map[int]int) {
    for k, v := range m {
        m[k] = v + 1
    }
}
func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }
    go func() {
        for i := 0; i < 1000; i++ {
            doIteration(m)
        }
    }()
    go func() {
        for i := 0; i < 1000; i++ {
            doWrite(m)
        }
    }()
    time.Sleep(5 * time.Second)
}

運(yùn)行這個(gè)示例程序,我們會(huì)得到下面的執(zhí)行錯(cuò)誤結(jié)果:

fatal error: concurrent map iteration and map write

不過(guò),如果我們僅僅是進(jìn)行并發(fā)讀,map 是沒(méi)有問(wèn)題的。而且,Go 1.9 版本中引入了支持并發(fā)寫安全的 sync.Map 類型,可以在并發(fā)讀寫的場(chǎng)景下替換掉 map。如果你有這方面的需求,可以查看一下sync.Map 的手冊(cè)。

另外,你要注意,考慮到 map 可以自動(dòng)擴(kuò)容,map 中數(shù)據(jù)元素的 value 位置可能在這一過(guò)程中發(fā)生變化,所以 Go 不允許獲取 map 中 value 的地址,這個(gè)約束是在編譯期間就生效的。下面這段代碼就展示了 Go 編譯器識(shí)別出獲取 map 中 value 地址的語(yǔ)句后,給出的編譯錯(cuò)誤:

p := &m[key]  // cannot take the address of m[key]
fmt.Println(p)

七、map 的基本操作

7.1 修改和更新鍵值對(duì)

首先 nil 的 map 類型變量,我們可以在其中插入符合 map 類型定義的任意新鍵值對(duì)。插入新鍵值對(duì)只需要把 value 賦值給 map 中對(duì)應(yīng)的 key 就可以了:

// 創(chuàng)建并初始化一個(gè) map
myMap := make(map[string]int)
myMap["apple"] = 1
myMap["banana"] = 2

不需要自己判斷數(shù)據(jù)有沒(méi)有插入成功,因?yàn)?Go 會(huì)保證插入總是成功的。不過(guò),如果我們插入新鍵值對(duì)的時(shí)候,某個(gè) key 已經(jīng)存在于 map 中了,那我們的插入操作就會(huì)用新值覆蓋舊值:

// 修改鍵 "apple" 對(duì)應(yīng)的值
myMap["apple"] = 3
// 更新鍵 "cherry" 對(duì)應(yīng)的值,如果鍵不存在則創(chuàng)建新鍵值對(duì)
myMap["cherry"] = 4
// 打印修改后的 map
fmt.Println(myMap) // 輸出: map[apple:3 banana:2 cherry:4]

從這段代碼中,您可以看到如何執(zhí)行以下操作:

  • 修改鍵 "apple" 對(duì)應(yīng)的值:使用myMap["apple"] = 3這行代碼,將鍵 "apple" 對(duì)應(yīng)的值從原來(lái)的 1 修改為 3。
  • 更新鍵 "cherry" 對(duì)應(yīng)的值:使用myMap["cherry"] = 4這行代碼,更新了鍵 "cherry" 對(duì)應(yīng)的值為 4。如果鍵 "cherry" 不存在于map中,這行代碼會(huì)創(chuàng)建一個(gè)新的鍵值對(duì)。
  • 打印修改后的 map:最后使用fmt.Println(myMap)打印整個(gè)修改后的map,以顯示更新后的鍵值對(duì)。

7.2 批量更新和修改(合并同類型map)

在Go中,可以使用循環(huán)遍歷另一個(gè)map,然后使用遍歷的鍵值對(duì)來(lái)批量更新或修改目標(biāo)map的鍵值對(duì)。以下是一個(gè)實(shí)現(xiàn)類似于Python字典的update()方法的步驟:

  • 創(chuàng)建一個(gè)目標(biāo)map,它將被更新或修改。
  • 創(chuàng)建一個(gè)源map,其中包含要合并到目標(biāo)map的鍵值對(duì)。
  • 遍歷源map的鍵值對(duì)。
  • 對(duì)于每個(gè)鍵值對(duì),檢查它是否存在于目標(biāo)map中。

    • 如果存在,將目標(biāo)map中的值更新為源map中的值。
    • 如果不存在,將源map中的鍵值對(duì)添加到目標(biāo)map中。
  • 最終,目標(biāo)map將包含源map中的所有鍵值對(duì)以及更新后的值。

以下是具體的Go代碼示例:

package main
import (
    "fmt"
)
func updateMap(target map[string]int, source map[string]int) {
    for key, value := range source {
        target[key] = value
    }
}
func main() {
    // 創(chuàng)建目標(biāo) map
    targetMap := map[string]int{
        "apple":  1,
        "banana": 2,
    }
    // 創(chuàng)建源 map,包含要更新或修改的鍵值對(duì)
    sourceMap := map[string]int{
        "apple":  3, // 更新 "apple" 的值為 3
        "cherry": 4, // 添加新的鍵值對(duì) "cherry": 4
    }
    // 調(diào)用 updateMap 函數(shù),將源 map 合并到目標(biāo) map 中
    updateMap(targetMap, sourceMap)
    // 打印更新后的目標(biāo) map
    fmt.Println(targetMap) // 輸出:map[apple:3 banana:2 cherry:4]
}

7.3 獲取鍵值對(duì)數(shù)量

要獲取一個(gè)map中鍵值對(duì)的數(shù)量(也稱為長(zhǎng)度),可以使用Go語(yǔ)言的len函數(shù)。len函數(shù)返回map中鍵值對(duì)的數(shù)量。以下是獲取map中鍵值對(duì)數(shù)量的示例:

// 創(chuàng)建并初始化一個(gè) map
    myMap := map[string]int{
        "apple":  1,
        "banana": 2,
        "cherry": 3,
    }
    // 使用 len 函數(shù)獲取 map 的鍵值對(duì)數(shù)量
    count := len(myMap)
    // 打印鍵值對(duì)數(shù)量
    fmt.Println("鍵值對(duì)數(shù)量:", count)

不過(guò),這里要注意的是我們不能對(duì) map 類型變量調(diào)用 cap,來(lái)獲取當(dāng)前容量,這是 map 類型與切片類型的一個(gè)不同點(diǎn)。

7.4 查找和數(shù)據(jù)讀?。ㄅ袛嗄硞€(gè)鍵是否存在)

7.4.1 查找和數(shù)據(jù)讀取 map 語(yǔ)法格式

Go語(yǔ)言中有個(gè)判斷map中鍵是否存在的特殊寫法,格式如下:

value, ok := map[key]

其中:

  • myMap 是目標(biāo)map,您希望在其中查找鍵。
  • key 是您要查找的鍵。
  • value 是一個(gè)變量,如果鍵存在,它將存儲(chǔ)鍵對(duì)應(yīng)的值,如果鍵不存在,則會(huì)獲得值類型的零值。
  • ok 是一個(gè)布爾值,用于指示鍵是否存在。如果鍵存在,oktrue;如果鍵不存在,okfalse。

map 類型更多用在查找和數(shù)據(jù)讀取場(chǎng)合。所謂查找,就是判斷某個(gè) key 是否存在于某個(gè) map 中。Go 語(yǔ)言的 map 類型支持通過(guò)用一種名為“comma ok”的慣用法,進(jìn)行對(duì)某個(gè) key 的查詢。接下來(lái)我們就用“comma ok”慣用法改造一下上面的代碼:

m := make(map[string]int)
v, ok := m["key1"]
if !ok {
    // "key1"不在map中
}
// "key1"在map中,v將被賦予"key1"鍵對(duì)應(yīng)的value

我們看到,這里我們通過(guò)了一個(gè)布爾類型變量 ok,來(lái)判斷鍵“key1”是否存在于 map 中。如果存在,變量 v 就會(huì)被正確地賦值為鍵“key1”對(duì)應(yīng)的 value。

不過(guò),如果我們并不關(guān)心某個(gè)鍵對(duì)應(yīng)的 value,而只關(guān)心某個(gè)鍵是否在于 map 中,我們可以使用空標(biāo)識(shí)符替代變量 v,忽略可能返回的 value:

m := make(map[string]int)
_, ok := m["key1"]
... ...

因此,你一定要記?。?strong>在 Go 語(yǔ)言中,請(qǐng)使用“comma ok”慣用法對(duì) map 進(jìn)行鍵查找和鍵值讀取操作。

7.4.2 實(shí)現(xiàn)get 方法查找map 對(duì)應(yīng)的key

在Go中,要實(shí)現(xiàn)類似Python字典的get()方法,可以編寫一個(gè)函數(shù),該函數(shù)接受一個(gè)map、一個(gè)鍵以及一個(gè)默認(rèn)值作為參數(shù)。函數(shù)將嘗試從map中獲取指定鍵的值,如果鍵不存在,則返回默認(rèn)值。以下是實(shí)現(xiàn)類似get()方法的步驟:

  • 創(chuàng)建一個(gè)函數(shù),命名為get,該函數(shù)接受三個(gè)參數(shù):map、鍵和默認(rèn)值。
  • 在函數(shù)中,使用鍵來(lái)嘗試從map中獲取對(duì)應(yīng)的值。
  • 如果值存在,返回該值;如果不存在,則返回默認(rèn)值空字符串。
package main
import (
    "fmt"
)
// 實(shí)現(xiàn)類似 Python 字典的 get() 方法
func get(myMap map[string]string, key string) string {
    value, ok := myMap[key]
    if !ok {
        return ""
    }
    return value
}
func main() {
    // 創(chuàng)建并初始化一個(gè) map
    myMap := map[string]string{
        "apple":  "red",
        "banana": "yellow",
        "cherry": "red",
    }
    // 使用 get() 方法獲取鍵 "apple" 的值,如果不存在返回空字符串
    appleValue := get(myMap, "apple")
    fmt.Println("Color of 'apple':", appleValue)
    // 使用 get() 方法獲取鍵 "tangerine" 的值,如果不存在返回空字符串
    grapeValue := get(myMap, "tangerine")
    if grapeValue == "" {
        fmt.Println("沒(méi)有獲取到tangerine的對(duì)應(yīng)的值!")
    } else {
        fmt.Println("Color of 'tangerine':", grapeValue)
    }
}

運(yùn)行此代碼將輸出:

Color of 'apple': red
沒(méi)有獲取到tangerine的對(duì)應(yīng)的值!

7.5 使用delete()函數(shù)刪除鍵值對(duì)

使用delete()內(nèi)建函數(shù)從map中刪除一組鍵值對(duì),delete()函數(shù)的格式如下:

delete(map, key)

其中:

  • map:表示要?jiǎng)h除鍵值對(duì)的map
  • key:表示要?jiǎng)h除的鍵值對(duì)的鍵

使用 delete 函數(shù)的情況下,傳入的第一個(gè)參數(shù)是我們的 map 類型變量,第二個(gè)參數(shù)就是我們想要?jiǎng)h除的鍵。我們可以看看這個(gè)代碼示例:

m := map[string]int {
  "key1" : 1,
  "key2" : 2,
}
fmt.Println(m) // map[key1:1 key2:2]
delete(m, "key2") // 刪除"key2"
fmt.Println(m) // map[key1:1]

7.6 遍歷 map 中的鍵值數(shù)據(jù)

最后,我們來(lái)說(shuō)一下如何遍歷 map 中的鍵值數(shù)據(jù)。這一點(diǎn)雖然不像查詢和讀取操作那么常見(jiàn),但日常開(kāi)發(fā)中我們還是有這個(gè)需求的。在 Go 中,遍歷 map 的鍵值對(duì)只有一種方法,那就是像對(duì)待切片那樣通過(guò) for range 語(yǔ)句對(duì) map 數(shù)據(jù)進(jìn)行遍歷。我們看一個(gè)例子:

package main
import "fmt"
func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }
    fmt.Printf("{ ")
    for k, v := range m {
        fmt.Printf("[%d, %d] ", k, v)
    }
    fmt.Printf("}\n")
}

你看,通過(guò) for range 遍歷 map 變量 m,每次迭代都會(huì)返回一個(gè)鍵值對(duì),其中鍵存在于變量 k 中,它對(duì)應(yīng)的值存儲(chǔ)在變量 v 中。我們可以運(yùn)行一下這段代碼,可以得到符合我們預(yù)期的結(jié)果:

{ [1, 11] [2, 12] [3, 13] }

如果我們只關(guān)心每次迭代的鍵,我們可以使用下面的方式對(duì) map 進(jìn)行遍歷:

for k, _ := range m { 
  // 使用k
}

當(dāng)然更地道的方式是這樣的:

for k := range m {
  // 使用k
}

如果我們只關(guān)心每次迭代返回的鍵所對(duì)應(yīng)的 value,我們同樣可以通過(guò)空標(biāo)識(shí)符替代變量 k,就像下面這樣:

for _, v := range m {
  // 使用v
}

不過(guò),前面 map 遍歷的輸出結(jié)果都非常理想,給我們的表象好像是迭代器按照 map 中元素的插入次序逐一遍歷。那事實(shí)是不是這樣呢?我們?cè)賮?lái)試試,多遍歷幾次這個(gè) map 看看。

我們先來(lái)改造一下代碼:

package main
import "fmt"
func doIteration(m map[int]int) {
    fmt.Printf("{ ")
    for k, v := range m {
        fmt.Printf("[%d, %d] ", k, v)
    }
    fmt.Printf("}\n")
}
func main() {
    m := map[int]int{
        1: 11,
        2: 12,
        3: 13,
    }
    for i := 0; i < 3; i++ {
        doIteration(m)
    }
}

運(yùn)行一下上述代碼,我們可以得到這樣結(jié)果:

{ [1, 11] [2, 12] [3, 13] }
{ [2, 12] [3, 13] [1, 11] }
{ [1, 11] [2, 12] [3, 13] }

我們可以看到,對(duì)同一 map 做多次遍歷的時(shí)候,每次遍歷元素的次序都不相同。這是 Go 語(yǔ)言 map 類型的一個(gè)重要特點(diǎn),也是很容易讓 Go 初學(xué)者掉入坑中的一個(gè)地方。所以這里你一定要記住:程序邏輯千萬(wàn)不要依賴遍歷 map 所得到的的元素次序。

八、Map的相等性

map 之間不能使用 == 操作符判斷,== 只能用來(lái)檢查 map 是否為 nil。

func main() {
    map1 := map[string]int{
        "one": 1,
        "two": 2,
    }
    map2 := map1
    if map1 ==nil{
        fmt.Println("map1為空")
    }else {
        fmt.Println("map1不為空")
    }
    if map1 == map2 { // 直接報(bào)錯(cuò),不能直接比較
    }
}

以上就是Go 復(fù)合類型之字典類型介紹的詳細(xì)內(nèi)容,更多關(guān)于Go 復(fù)合類型之字典類型介紹的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Go語(yǔ)言中循環(huán)Loop的用法介紹

    Go語(yǔ)言中循環(huán)Loop的用法介紹

    這篇文章介紹了Go語(yǔ)言中循環(huán)Loop的用法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2022-07-07
  • 一文了解Go語(yǔ)言的并發(fā)特性

    一文了解Go語(yǔ)言的并發(fā)特性

    本文主要介紹了一文了解Go語(yǔ)言的并發(fā)特性,通過(guò)輕量級(jí)線程、通道及選擇語(yǔ)句,使得并發(fā)編程變得既簡(jiǎn)單又高效,下面就來(lái)具體了解一下如何使用,感興趣的可以了解一下
    2024-02-02
  • Windows系統(tǒng)中搭建Go語(yǔ)言開(kāi)發(fā)環(huán)境圖文詳解

    Windows系統(tǒng)中搭建Go語(yǔ)言開(kāi)發(fā)環(huán)境圖文詳解

    GoLand?是?JetBrains?公司推出的商業(yè)?Go?語(yǔ)言集成開(kāi)發(fā)環(huán)境(IDE),這篇文章主要介紹了Windows系統(tǒng)中搭建Go語(yǔ)言開(kāi)發(fā)環(huán)境詳解,需要的朋友可以參考下
    2022-10-10
  • go語(yǔ)言實(shí)現(xiàn)二叉樹(shù)的序例化與反序列化

    go語(yǔ)言實(shí)現(xiàn)二叉樹(shù)的序例化與反序列化

    這篇文章主要介紹了go語(yǔ)言實(shí)現(xiàn)二叉樹(shù)的序例化與反序列化,文章圍繞主題展開(kāi)詳細(xì)內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下
    2022-05-05
  • Golang 實(shí)現(xiàn)獲取當(dāng)前函數(shù)名稱和文件行號(hào)等操作

    Golang 實(shí)現(xiàn)獲取當(dāng)前函數(shù)名稱和文件行號(hào)等操作

    這篇文章主要介紹了Golang 實(shí)現(xiàn)獲取當(dāng)前函數(shù)名稱和文件行號(hào)等操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧
    2021-05-05
  • 詳解Go語(yǔ)言中select語(yǔ)句的常見(jiàn)用法

    詳解Go語(yǔ)言中select語(yǔ)句的常見(jiàn)用法

    這篇文章主要是來(lái)和大家介紹一下Go語(yǔ)言中select?語(yǔ)句的常見(jiàn)用法,以及在使用過(guò)程中的注意事項(xiàng),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以了解一下
    2023-07-07
  • 使用go語(yǔ)言將單反斜杠改為雙反斜杠的方法

    使用go語(yǔ)言將單反斜杠改為雙反斜杠的方法

    最近開(kāi)發(fā)的時(shí)候遇到這么個(gè)問(wèn)題,就是在window上獲取了文件目錄的字段,然后將這個(gè)絕對(duì)路徑保存到數(shù)據(jù)庫(kù),但是前端展示的時(shí)候路徑的雙反斜杠變成了單反斜杠,本文給大家介紹了使用go語(yǔ)言將單反斜杠改為雙反斜杠的方法,需要的朋友可以參考下
    2024-01-01
  • 詳解go語(yǔ)言 make(chan int, 1) 和 make (chan int) 的區(qū)別

    詳解go語(yǔ)言 make(chan int, 1) 和 make (chan int) 的區(qū)別

    這篇文章主要介紹了go語(yǔ)言 make(chan int, 1) 和 make (chan int) 的區(qū)別,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2020-01-01
  • golang如何替換換行符

    golang如何替換換行符

    這篇文章主要介紹了golang如何替換換行符問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • 利用dep代替go get獲取私有庫(kù)的方法教程

    利用dep代替go get獲取私有庫(kù)的方法教程

    go get 從指定源上面下載或者更新指定的代碼和依賴,并對(duì)他們進(jìn)行編譯和安裝,但go get功能比較差,所以下面這篇文章主要給大家介紹了關(guān)于利用dep代替go get獲取私有庫(kù)的相關(guān)資料,需要的朋友可以參考借鑒,下面來(lái)一起看看吧。
    2017-11-11

最新評(píng)論