Go?復(fù)合類型之字典類型使用教程示例
一、map類型介紹
1.1 什么是 map 類型?
map 是 Go 語言提供的一種抽象數(shù)據(jù)類型,它表示一組無序的鍵值對。用 key 和 value 分別代表 map 的鍵和值。而且,map 集合中每個 key 都是唯一的:
和切片類似,作為復(fù)合類型的 map
,它在 Go
中的類型表示也是由 key
類型與 value 類型組成的,就像下面代碼:
map[key_type]value_type
key 與 value 的類型可以相同,也可以不同:
map[string]string // key與value元素的類型相同 map[int]string // key與value元素的類型不同
如果兩個 map 類型的 key 元素類型相同,value 元素類型也相同,那么我們可以說它們是同一個 map 類型,否則就是不同的 map 類型。
這里,我們要注意,map 類型對 value 的類型沒有限制,但是對 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 // 編譯錯誤 // map類型不能作為key m1 := make(map[string]int) m[m1] = 1 // 編譯錯誤 // 切片類型不能作為key,因?yàn)榍衅强勺冮L度的,它們的內(nèi)容可能會在運(yùn)行時更改 s1 := []int{1,2,3} m[s1] = 1 // 編譯錯誤
上面代碼中,試圖使用函數(shù)類型、map類型和切片類型作為key都會導(dǎo)致編譯錯誤。
這是因?yàn)镚o語言在實(shí)現(xiàn)map時,需要比較key是否相等,因此key需要支持==比較。但函數(shù)、map和切片類型的相等性比較涉及內(nèi)存地址,無法簡單判斷,所以不能作為key。所以,key 的類型必須支持“==”和“!=”兩種比較操作符。
還需要注意的是,在 Go 語言中,函數(shù)類型、map 類型自身,以及切片只支持與 nil 的比較,而不支持同類型兩個變量的比較。如果像下面代碼這樣,進(jìn)行這些類型的比較,Go 編譯器將會報錯:
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) // 錯誤:invalid operation: s1 == s2 (slice can only be compared to nil) println(f1 == f2) // 錯誤:invalid operation: f1 == f2 (func can only be compared to nil) println(m1 == m2) // 錯誤:invalid operation: m1 == m2 (map can only be compared to nil)
1.2 map 類型特性
在Go中,map
具有以下特性:
- 無序性:
map
中的鍵值對沒有固定的順序,遍歷時可能不按照添加的順序返回鍵值對。 - 動態(tài)增長:
map
是動態(tài)的,它會根據(jù)需要自動增長以容納更多的鍵值對,不需要預(yù)先指定大小。 - 零值: 如果未初始化一個
map
,它將是nil
,并且不能存儲鍵值對。需要使用make
函數(shù)來初始化一個map
。 - 鍵的唯一性: 在同一個
map
中,每個鍵只能出現(xiàn)一次。如果嘗試使用相同的鍵插入多次,新值將覆蓋舊值。 - 查詢效率高:
map
的查詢操作通常非???,因?yàn)樗褂霉1韥泶鎯?shù)據(jù),這使得通過鍵查找值的時間復(fù)雜度接近常數(shù)。 - 引用類型:
map
是一種引用類型,多個變量可以引用并共享同一個map
實(shí)例。
二.map 變量的聲明和初始化
和切片一樣,為 map 類型變量顯式賦值有兩種方式:一種是使用復(fù)合字面值;另外一種是使用 make 這個預(yù)聲明的內(nèi)置函數(shù)。
2.1 方法一:使用 make 函數(shù)聲明和初始化(推薦)
這是最常見和推薦的方式,特別是在需要在map
中添加鍵值對之前初始化map
的情況下。使用make
函數(shù)可以為map
分配內(nèi)存并進(jìn)行初始化。
// 使用 make 函數(shù)聲明和初始化 map myMap := make(map[keyType]valueType,capacity)
其中:
keyType
是鍵的類型。valueType
是值的類型。- capacity表示
map
的初始容量,它是可選的,可以省略不寫。
例如:和切片通過 make
進(jìn)行初始化一樣,通過 make
的初始化方式,我們可以為 map
類型變量指定鍵值對的初始容量,但無法進(jìn)行具體的鍵值對賦值,就像下面代碼這樣:
// 創(chuàng)建一個存儲整數(shù)到字符串的映射 m1 := make(map[int]string) // 未指定初始容量 m1[1] = "key" fmt.Println(m1)
map 類型的容量不會受限于它的初始容量值,當(dāng)其中的鍵值對數(shù)量超過初始容量后,Go 運(yùn)行時會自動增加 map
類型的容量,保證后續(xù)鍵值對的正常插入,比如下面這段代碼:
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] ,并不會報錯 fmt.Println(len(m2)) // 此時,map容量已經(jīng)變?yōu)?
總結(jié):使用make
函數(shù)初始化的map
是空的,需要在后續(xù)代碼中添加鍵值對。
mm := make(map[int]string) fmt.Println(mm) // 輸出 map[]
2.2 方法二:使用復(fù)合字面值聲明初始化 map 類型變量
和切片類型變量一樣,如果我們沒有顯式地賦予 map 變量初值,map 類型變量的默認(rèn)值為 nil
,比如,我們來看下面這段代碼:
var m map[string]int if m == nil { fmt.Println("Map is nil") } else { fmt.Println("Map is not nil") }
不過切片變量和 map 變量在這里也有些不同。初值為零值 nil 的切片類型變量,可以借助內(nèi)置的 append 的函數(shù)進(jìn)行操作,這種在 Go 語言中被稱為“零值可用”。定義“零值可用”的類型,可以提升我們開發(fā)者的使用體驗(yàn),我們不用再擔(dān)心變量的初始狀態(tài)是否有效。比如,創(chuàng)建一個存儲字符串到整數(shù)的映射,但 map 類型,因?yàn)樗鼉?nèi)部實(shí)現(xiàn)的復(fù)雜性,無法“零值可用”。所以,如果我們對處于零值狀態(tài)的 map 變量直接進(jìn)行操作,就會導(dǎo)致運(yùn)行時異常(panic),從而導(dǎo)致程序進(jìn)程異常退出:
var m map[string]int // m = nil m["key"] = 1 // 發(fā)生運(yùn)行時異常:panic: assignment to entry in nil map
所以,我們必須對 map 類型變量進(jìn)行顯式初始化后才能使用。我們先來看這句代碼:
m := map[int]string{}
這里,我們顯式初始化了 map 類型變量 m。不過,你要注意,雖然此時 map 類型變量 m 中沒有任何鍵值對,但變量 m 也不等同于初值為 nil 的 map 變量。這個時候,我們對 m 進(jìn)行鍵值對的插入操作,不會引發(fā)運(yùn)行時異常。
這里我們再看看怎么通過稍微復(fù)雜一些的復(fù)合字面值,對 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", }
我們看到,上面代碼雖然完成了對兩個 map 類型變量 m1 和 m2 的顯式初始化,但不知道你有沒有發(fā)現(xiàn)一個問題,作為初值的字面值似乎有些“臃腫”。你看,作為初值的字面值采用了復(fù)合類型的元素類型,而且在編寫字面值時還帶上了各自的元素類型,比如作為 map[int] []string
值類型的[]string
,以及作為 map[Position]string
的 key 類型的 Position。
別急!針對這種情況,Go 提供了“語法糖”。這種情況下,Go 允許省略字面值中的元素類型。因?yàn)?map 類型表示中包含了 key 和 value 的元素類型,Go 編譯器已經(jīng)有足夠的信息,來推導(dǎo)出字面值中各個值的類型了。我們以 m2 為例,這里的顯式初始化代碼和上面變量 m2 的初始化代碼是等價的:
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
是值的類型- 然后使用大括號
{}
包圍鍵值對
三.map 變量的傳遞開銷(map是引用傳遞)
和切片類型一樣,map 也是引用類型。這就意味著 map 類型變量作為參數(shù)被傳遞給函數(shù)或方法的時候,實(shí)質(zhì)上傳遞的只是一個“描述符”,而不是整個 map 的數(shù)據(jù)拷貝,所以這個傳遞的開銷是固定的,而且也很小。
并且,當(dāng) map 變量被傳遞到函數(shù)或方法內(nèi)部后,我們在函數(shù)內(nèi)部對 map 類型參數(shù)的修改在函數(shù)外部也是可見的。比如你從這個示例中就可以看到,函數(shù) foo 中對 map 類型變量 m 進(jìn)行了修改,而這些修改在 foo 函數(shù)外也可見。
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 被賦值為一個新變量的時候,它們指向同一個內(nèi)部數(shù)據(jù)結(jié)構(gòu)。因此,當(dāng)改變其中一個變量,就會影響到另一變量。
四.map 的內(nèi)部實(shí)現(xiàn)
4.1 map 類型在 Go 運(yùn)行時層實(shí)現(xiàn)的示意圖
和切片相比,map 類型的內(nèi)部實(shí)現(xiàn)要更加復(fù)雜。Go 運(yùn)行時使用一張哈希表來實(shí)現(xiàn)抽象的 map 類型。運(yùn)行時實(shí)現(xiàn)了 map 類型操作的所有功能,包括查找、插入、刪除等。在編譯階段,Go 編譯器會將 Go 語法層面的 map 操作,重寫成運(yùn)行時對應(yīng)的函數(shù)調(diào)用。大致的對應(yīng)關(guān)系是這樣的:
// 創(chuàng)建map類型變量實(shí)例 m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m) // 插入新鍵值對或給鍵重新賦值 m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") v是用于后續(xù)存儲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í)現(xiàn)的示意圖:
我們可以看到,和切片的運(yùn)行時表示圖相比,map 的實(shí)現(xiàn)示意圖顯然要復(fù)雜得多。接下來,我們結(jié)合這張圖來簡要描述一下 map 在運(yùn)行時層的實(shí)現(xiàn)原理。接下來我們來看一下一個 map 變量在初始狀態(tài)、進(jìn)行鍵值對操作后,以及在并發(fā)場景下的 Go 運(yùn)行時層的實(shí)現(xiàn)原理。
4.2 初始狀態(tài)
從圖中我們可以看到,與語法層面 map 類型變量(m)一一對應(yīng)的是 *runtime.hmap
的實(shí)例,即 runtime.hmap
類型的指針,也就是我們前面在講解 map 類型變量傳遞開銷時提到的 map 類型的描述符。hmap 類型是 map 類型的頭部結(jié)構(gòu)(header
),它存儲了后續(xù) map
類型操作所需的所有信息,包括:
真正用來存儲鍵值對數(shù)據(jù)的是桶,也就是 bucket,每個 bucket 中存儲的是 Hash 值低 bit 位數(shù)值相同的元素,默認(rèn)的元素個數(shù)為 BUCKETSIZE(值為 8,Go 1.17 版本中在 $GOROOT/src/cmd/compile/internal/reflectdata/reflect.go
中定義,與 runtime/map.go
中常量 bucketCnt
保持一致)。
當(dāng)某個 bucket(比如 buckets[0]) 的 8 個空槽 slot)都填滿了,且 map 尚未達(dá)到擴(kuò)容的條件的情況下,運(yùn)行時會建立 overflow bucket,并將這個 overflow bucket 掛在上面 bucket(如 buckets[0])末尾的 overflow 指針上,這樣兩個 buckets 形成了一個鏈表結(jié)構(gòu),直到下一次 map 擴(kuò)容之前,這個結(jié)構(gòu)都會一直存在。
從圖中我們可以看到,每個 bucket 由三部分組成,從上到下分別是 tophash 區(qū)域、key 存儲區(qū)域和 value 存儲區(qū)域。
4.3 tophash 區(qū)域
當(dāng)我們向 map
插入一條數(shù)據(jù),或者是從 map
按 key
查詢數(shù)據(jù)的時候,運(yùn)行時都會使用哈希函數(shù)對 key
做哈希運(yùn)算,并獲得一個哈希值(hashcode)
。這個 hashcode
非常關(guān)鍵,運(yùn)行時會把 hashcode
“一分為二”來看待,其中低位區(qū)的值用于選定 bucket
,高位區(qū)的值用于在某個 bucket
中確定 key
的位置。我把這一過程整理成了下面這張示意圖,你理解起來可以更直觀:
因此,每個 bucket 的 tophash 區(qū)域其實(shí)是用來快速定位 key 位置的,這樣就避免了逐個 key 進(jìn)行比較這種代價較大的操作。尤其是當(dāng) key 是 size 較大的字符串類型時,好處就更突出了。這是一種以空間換時間的思路。
4.4 key 存儲區(qū)域
接著,我們看 tophash 區(qū)域下面是一塊連續(xù)的內(nèi)存區(qū)域,存儲的是這個 bucket 承載的所有 key 數(shù)據(jù)。運(yùn)行時在分配 bucket 的時候需要知道 key 的 Size。那么運(yùn)行時是如何知道 key 的 size 的呢?
當(dāng)我們聲明一個 map 類型變量,比如 var m map[string]int
時,Go 運(yùn)行時就會為這個變量對應(yīng)的特定 map 類型,生成一個 runtime.maptype
實(shí)例。如果這個實(shí)例已經(jīng)存在,就會直接復(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 }
我們可以看到,這個實(shí)例包含了我們需要的 map 類型中的所有"元信息"。我們前面提到過,編譯器會把語法層面的 map 操作重寫成運(yùn)行時對應(yīng)的函數(shù)調(diào)用,這些運(yùn)行時函數(shù)都有一個共同的特點(diǎn),那就是第一個參數(shù)都是 maptype 指針類型的參數(shù)。
Go 運(yùn)行時就是利用 maptype 參數(shù)中的信息確定 key 的類型和大小的。map
所用的 hash 函數(shù)也存放在 maptype.key.alg.hash(key, hmap.hash0)
中。同時 maptype 的存在也讓 Go 中所有 map 類型都共享一套運(yùn)行時 map 操作函數(shù),而不是像 C++
那樣為每種 map
類型創(chuàng)建一套 map
操作函數(shù),這樣就節(jié)省了對最終二進(jìn)制文件空間的占用。
4.5 value 存儲區(qū)域
我們再接著看 key 存儲區(qū)域下方的另外一塊連續(xù)的內(nèi)存區(qū)域,這個區(qū)域存儲的是 key 對應(yīng)的 value
。和 key
一樣,這個區(qū)域的創(chuàng)建也是得到了 maptype
中信息的幫助。Go 運(yùn)行時采用了把 key
和 value
分開存儲的方式,而不是采用一個 kv
接著一個 kv
的 kv
緊鄰方式存儲,這帶來的其實(shí)是算法上的復(fù)雜性,但卻減少了因內(nèi)存對齊帶來的內(nèi)存浪費(fèi)。
我們以 map[int8]int64
為例,看看下面的存儲空間利用率對比圖:
你會看到,當(dāng)前 Go 運(yùn)行時使用的方案內(nèi)存利用效率很高,而 kv 緊鄰存儲的方案在 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ù)長度大于一定數(shù)值,那么運(yùn)行時不會在 bucket 中直接存儲數(shù)據(jù),而是會存儲 key 或 value 數(shù)據(jù)的指針。目前 Go 運(yùn)行時定義的最大 key 和 value 的長度是這樣的:
// $GOROOT/src/runtime/map.go const ( maxKeySize = 128 maxElemSize = 128 )
五.map 擴(kuò)容
我們前面提到過,map 會對底層使用的內(nèi)存進(jìn)行自動管理。因此,在使用過程中,當(dāng)插入元素個數(shù)超出一定數(shù)值后,map 一定會存在自動擴(kuò)容的問題,也就是怎么擴(kuò)充 bucket 的數(shù)量,并重新在 bucket 間均衡分配數(shù)據(jù)的問題。
那么 map 在什么情況下會進(jìn)行擴(kuò)容呢?Go 運(yùn)行時的 map 實(shí)現(xiàn)中引入了一個 LoadFactor
(負(fù)載因子),當(dāng) count > LoadFactor * 2^B 或 overflow bucket
過多時,運(yùn)行時會自動對 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í)是不一樣的。如果是因?yàn)?overflow bucket 過多導(dǎo)致的“擴(kuò)容”,實(shí)際上運(yùn)行時會新建一個和現(xiàn)有規(guī)模一樣的 bucket 數(shù)組,然后在 assign 和 delete 時做排空和遷移。
如果是因?yàn)楫?dāng)前數(shù)據(jù)數(shù)量超出 LoadFactor 指定水位而進(jìn)行的擴(kuò)容,那么運(yùn)行時會建立一個兩倍于現(xiàn)有規(guī)模的 bucket 數(shù)組,但真正的排空和遷移工作也是在 assign 和 delete 時逐步進(jìn)行的。原 bucket 數(shù)組會掛在 hmap 的 oldbuckets 指針下面,直到原 buckets 數(shù)組中所有數(shù)據(jù)都遷移到新數(shù)組后,原 buckets 數(shù)組才會被釋放。你可以結(jié)合下面的 map 擴(kuò)容示意圖來理解這個過程,這會讓你理解得更深刻一些:
六.map 與并發(fā)
接著我們來看一下 map 和并發(fā)。從上面的實(shí)現(xiàn)原理來看,充當(dāng) map 描述符角色的 hmap 實(shí)例自身是有狀態(tài)的(hmap.flags),而且對狀態(tài)的讀寫是沒有并發(fā)保護(hù)的。所以說 map 實(shí)例不是并發(fā)寫安全的,也不支持并發(fā)讀寫。如果我們對 map 實(shí)例進(jìn)行并發(fā)讀寫,程序運(yùn)行時就會拋出異常。你可以看看下面這個并發(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)行這個示例程序,我們會得到下面的執(zhí)行錯誤結(jié)果:
fatal error: concurrent map iteration and map write
不過,如果我們僅僅是進(jìn)行并發(fā)讀,map 是沒有問題的。而且,Go 1.9 版本中引入了支持并發(fā)寫安全的 sync.Map 類型,可以在并發(fā)讀寫的場景下替換掉 map。如果你有這方面的需求,可以查看一下sync.Map 的手冊。
另外,你要注意,考慮到 map 可以自動擴(kuò)容,map 中數(shù)據(jù)元素的 value 位置可能在這一過程中發(fā)生變化,所以 Go 不允許獲取 map 中 value 的地址,這個約束是在編譯期間就生效的。下面這段代碼就展示了 Go 編譯器識別出獲取 map 中 value 地址的語句后,給出的編譯錯誤:
p := &m[key] // cannot take the address of m[key] fmt.Println(p)
七、map 的基本操作
7.1 修改和更新鍵值對
首先 nil 的 map 類型變量,我們可以在其中插入符合 map 類型定義的任意新鍵值對。插入新鍵值對只需要把 value 賦值給 map 中對應(yīng)的 key 就可以了:
// 創(chuàng)建并初始化一個 map myMap := make(map[string]int) myMap["apple"] = 1 myMap["banana"] = 2
不需要自己判斷數(shù)據(jù)有沒有插入成功,因?yàn)?Go 會保證插入總是成功的。不過,如果我們插入新鍵值對的時候,某個 key 已經(jīng)存在于 map 中了,那我們的插入操作就會用新值覆蓋舊值:
// 修改鍵 "apple" 對應(yīng)的值 myMap["apple"] = 3 // 更新鍵 "cherry" 對應(yīng)的值,如果鍵不存在則創(chuàng)建新鍵值對 myMap["cherry"] = 4 // 打印修改后的 map fmt.Println(myMap) // 輸出: map[apple:3 banana:2 cherry:4]
從這段代碼中,您可以看到如何執(zhí)行以下操作:
- 修改鍵 "apple" 對應(yīng)的值:使用
myMap["apple"] = 3
這行代碼,將鍵 "apple" 對應(yīng)的值從原來的 1 修改為 3。 - 更新鍵 "cherry" 對應(yīng)的值:使用
myMap["cherry"] = 4
這行代碼,更新了鍵 "cherry" 對應(yīng)的值為 4。如果鍵 "cherry" 不存在于map
中,這行代碼會創(chuàng)建一個新的鍵值對。 - 打印修改后的 map:最后使用
fmt.Println(myMap)
打印整個修改后的map
,以顯示更新后的鍵值對。
7.2 批量更新和修改(合并同類型map)
在Go中,可以使用循環(huán)遍歷另一個map
,然后使用遍歷的鍵值對來批量更新或修改目標(biāo)map
的鍵值對。以下是一個實(shí)現(xiàn)類似于Python字典的update()
方法的步驟:
- 創(chuàng)建一個目標(biāo)
map
,它將被更新或修改。 - 創(chuàng)建一個源
map
,其中包含要合并到目標(biāo)map
的鍵值對。 - 遍歷源
map
的鍵值對。 對于每個鍵值對,檢查它是否存在于目標(biāo)
map
中。- 如果存在,將目標(biāo)
map
中的值更新為源map
中的值。 - 如果不存在,將源
map
中的鍵值對添加到目標(biāo)map
中。
- 如果存在,將目標(biāo)
- 最終,目標(biāo)
map
將包含源map
中的所有鍵值對以及更新后的值。
以下是具體的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,包含要更新或修改的鍵值對 sourceMap := map[string]int{ "apple": 3, // 更新 "apple" 的值為 3 "cherry": 4, // 添加新的鍵值對 "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 獲取鍵值對數(shù)量
要獲取一個map
中鍵值對的數(shù)量(也稱為長度),可以使用Go語言的len
函數(shù)。len
函數(shù)返回map
中鍵值對的數(shù)量。以下是獲取map
中鍵值對數(shù)量的示例:
// 創(chuàng)建并初始化一個 map myMap := map[string]int{ "apple": 1, "banana": 2, "cherry": 3, } // 使用 len 函數(shù)獲取 map 的鍵值對數(shù)量 count := len(myMap) // 打印鍵值對數(shù)量 fmt.Println("鍵值對數(shù)量:", count)
不過,這里要注意的是我們不能對 map 類型變量調(diào)用 cap,來獲取當(dāng)前容量,這是 map 類型與切片類型的一個不同點(diǎn)。
7.4 查找和數(shù)據(jù)讀?。ㄅ袛嗄硞€鍵是否存在)
7.4.1 查找和數(shù)據(jù)讀取 map 語法格式
Go語言中有個判斷map中鍵是否存在的特殊寫法,格式如下:
value, ok := map[key]
其中:
myMap
是目標(biāo)map
,您希望在其中查找鍵。key
是您要查找的鍵。value
是一個變量,如果鍵存在,它將存儲鍵對應(yīng)的值,如果鍵不存在,則會獲得值類型的零值。ok
是一個布爾值,用于指示鍵是否存在。如果鍵存在,ok
為true
;如果鍵不存在,ok
為false
。
map 類型更多用在查找和數(shù)據(jù)讀取場合。所謂查找,就是判斷某個 key 是否存在于某個 map 中。Go 語言的 map 類型支持通過用一種名為“comma ok”的慣用法,進(jìn)行對某個 key 的查詢。接下來我們就用“comma ok”慣用法改造一下上面的代碼:
m := make(map[string]int) v, ok := m["key1"] if !ok { // "key1"不在map中 } // "key1"在map中,v將被賦予"key1"鍵對應(yīng)的value
我們看到,這里我們通過了一個布爾類型變量 ok,來判斷鍵“key1”是否存在于 map 中。如果存在,變量 v 就會被正確地賦值為鍵“key1”對應(yīng)的 value。
不過,如果我們并不關(guān)心某個鍵對應(yīng)的 value,而只關(guān)心某個鍵是否在于 map 中,我們可以使用空標(biāo)識符替代變量 v,忽略可能返回的 value:
m := make(map[string]int) _, ok := m["key1"] ... ...
因此,你一定要記住:在 Go 語言中,請使用“comma ok”慣用法對 map 進(jìn)行鍵查找和鍵值讀取操作。
7.4.2 實(shí)現(xiàn)get 方法查找map 對應(yīng)的key
在Go中,要實(shí)現(xiàn)類似Python字典的get()
方法,可以編寫一個函數(shù),該函數(shù)接受一個map
、一個鍵以及一個默認(rèn)值作為參數(shù)。函數(shù)將嘗試從map
中獲取指定鍵的值,如果鍵不存在,則返回默認(rèn)值。以下是實(shí)現(xiàn)類似get()
方法的步驟:
- 創(chuàng)建一個函數(shù),命名為
get
,該函數(shù)接受三個參數(shù):map
、鍵和默認(rèn)值。 - 在函數(shù)中,使用鍵來嘗試從
map
中獲取對應(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)建并初始化一個 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("沒有獲取到tangerine的對應(yīng)的值!") } else { fmt.Println("Color of 'tangerine':", grapeValue) } }
運(yùn)行此代碼將輸出:
Color of 'apple': red
沒有獲取到tangerine的對應(yīng)的值!
7.5 使用delete()函數(shù)刪除鍵值對
使用delete()
內(nèi)建函數(shù)從map中刪除一組鍵值對,delete()
函數(shù)的格式如下:
delete(map, key)
其中:
- map:表示要刪除鍵值對的map
- key:表示要刪除的鍵值對的鍵
使用 delete 函數(shù)的情況下,傳入的第一個參數(shù)是我們的 map 類型變量,第二個參數(shù)就是我們想要刪除的鍵。我們可以看看這個代碼示例:
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ù)
最后,我們來說一下如何遍歷 map 中的鍵值數(shù)據(jù)。這一點(diǎn)雖然不像查詢和讀取操作那么常見,但日常開發(fā)中我們還是有這個需求的。在 Go 中,遍歷 map 的鍵值對只有一種方法,那就是像對待切片那樣通過 for range 語句對 map 數(shù)據(jù)進(jìn)行遍歷。我們看一個例子:
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") }
你看,通過 for range 遍歷 map 變量 m,每次迭代都會返回一個鍵值對,其中鍵存在于變量 k 中,它對應(yīng)的值存儲在變量 v 中。我們可以運(yùn)行一下這段代碼,可以得到符合我們預(yù)期的結(jié)果:
{ [1, 11] [2, 12] [3, 13] }
如果我們只關(guān)心每次迭代的鍵,我們可以使用下面的方式對 map 進(jìn)行遍歷:
for k, _ := range m { // 使用k }
當(dāng)然更地道的方式是這樣的:
for k := range m { // 使用k }
如果我們只關(guān)心每次迭代返回的鍵所對應(yīng)的 value,我們同樣可以通過空標(biāo)識符替代變量 k,就像下面這樣:
for _, v := range m { // 使用v }
不過,前面 map 遍歷的輸出結(jié)果都非常理想,給我們的表象好像是迭代器按照 map 中元素的插入次序逐一遍歷。那事實(shí)是不是這樣呢?我們再來試試,多遍歷幾次這個 map 看看。
我們先來改造一下代碼:
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] }
我們可以看到,對同一 map 做多次遍歷的時候,每次遍歷元素的次序都不相同。這是 Go 語言 map 類型的一個重要特點(diǎn),也是很容易讓 Go 初學(xué)者掉入坑中的一個地方。所以這里你一定要記?。?strong>程序邏輯千萬不要依賴遍歷 map 所得到的的元素次序。
八、Map的相等性
map 之間不能使用 ==
操作符判斷,==
只能用來檢查 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 { // 直接報錯,不能直接比較 } }
以上就是Go 復(fù)合類型之字典類型介紹的詳細(xì)內(nèi)容,更多關(guān)于Go 復(fù)合類型之字典類型介紹的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Windows系統(tǒng)中搭建Go語言開發(fā)環(huán)境圖文詳解
GoLand?是?JetBrains?公司推出的商業(yè)?Go?語言集成開發(fā)環(huán)境(IDE),這篇文章主要介紹了Windows系統(tǒng)中搭建Go語言開發(fā)環(huán)境詳解,需要的朋友可以參考下2022-10-10Golang 實(shí)現(xiàn)獲取當(dāng)前函數(shù)名稱和文件行號等操作
這篇文章主要介紹了Golang 實(shí)現(xiàn)獲取當(dāng)前函數(shù)名稱和文件行號等操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2021-05-05詳解go語言 make(chan int, 1) 和 make (chan int) 的區(qū)別
這篇文章主要介紹了go語言 make(chan int, 1) 和 make (chan int) 的區(qū)別,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-01-01