Go 複合型別之字典型別介紹
一、map型別介紹
1.1 什麼是 map 型別?
map 是 Go 語言提供的一種抽象資料型別,它表示一組無序的鍵值對。用 key 和 value 分別代表 map 的鍵和值。而且,map 集合中每個 key 都是唯一的:
和切片類似,作為複合型別的 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 的型別卻有嚴格要求,因為 map 型別要保證 key 的唯一性。因此在這裡,你一定要注意:函式型別、map 型別自身,以及切片型別是不能作為 map 的 key 型別的。比如下面這段程式碼:
// 函式型別不能作為key,因為函式型別是不可比較的
func keyFunc() {}
m := make(map[string]int)
m[keyFunc] = 1 // 編譯錯誤
// map型別不能作為key
m1 := make(map[string]int)
m[m1] = 1 // 編譯錯誤
// 切片型別不能作為key,因為切片是可變長度的,它們的內容可能會在執行時更改
s1 := []int{1,2,3}
m[s1] = 1 // 編譯錯誤
上面程式碼中,試圖使用函式型別、map型別和切片型別作為key都會導致編譯錯誤。
這是因為Go語言在實現map時,需要比較key是否相等,因此key需要支援比較。但函式、map和切片型別的相等性比較涉及記憶體地址,無法簡單判斷,所以不能作為key。**所以,key 的型別必須支援“”和“!=”兩種比較運算子**。
還需要注意的是,在 Go 語言中,函式型別、map 型別自身,以及切片只支援與 nil 的比較,而不支援同型別兩個變數的比較。如果像下面程式碼這樣,進行這些型別的比較,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
中的鍵值對沒有固定的順序,遍歷時可能不按照新增的順序返回鍵值對。 - 動態增長:
map
是動態的,它會根據需要自動增長以容納更多的鍵值對,不需要預先指定大小。 - 零值: 如果未初始化一個
map
,它將是nil
,並且不能儲存鍵值對。需要使用make
函式來初始化一個map
。 - 鍵的唯一性: 在同一個
map
中,每個鍵只能出現一次。如果嘗試使用相同的鍵插入多次,新值將覆蓋舊值。 - 查詢效率高:
map
的查詢操作通常非常快,因為它使用雜湊表來儲存資料,這使得透過鍵查詢值的時間複雜度接近常數。 - 引用型別:
map
是一種引用型別,多個變數可以引用並共享同一個map
例項。
二.map 變數的宣告和初始化
和切片一樣,為 map 型別變數顯式賦值有兩種方式:一種是使用複合字面值;另外一種是使用 make 這個預宣告的內建函式。
2.1 方法一:使用 make
函式宣告和初始化(推薦)
這是最常見和推薦的方式,特別是在需要在map
中新增鍵值對之前初始化map
的情況下。使用make
函式可以為map
分配記憶體並進行初始化。
// 使用 make 函式宣告和初始化 map
myMap := make(map[keyType]valueType,capacity)
其中:
-
keyType
是鍵的型別。 -
valueType
是值的型別。 -
capacity表示
map
的初始容量,它是可選的,可以省略不寫。
例如:和切片透過 make
進行初始化一樣,透過 make
的初始化方式,我們可以為 map
型別變數指定鍵值對的初始容量,但無法進行具體的鍵值對賦值,就像下面程式碼這樣:
// 建立一個儲存整數到字串的對映
m1 := make(map[int]string) // 未指定初始容量
m1[1] = "key"
fmt.Println(m1)
map 型別的容量不會受限於它的初始容量值,當其中的鍵值對數量超過初始容量後,Go 執行時會自動增加 map
型別的容量,保證後續鍵值對的正常插入,比如下面這段程式碼:
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容量已經變為3
總結:使用make
函式初始化的map
是空的,需要在後續程式碼中新增鍵值對。
mm := make(map[int]string)
fmt.Println(mm) // 輸出 map[]
2.2 方法二:使用複合字面值宣告初始化 map 型別變數
和切片型別變數一樣,如果我們沒有顯式地賦予 map 變數初值,map 型別變數的預設值為 nil
,比如,我們來看下面這段程式碼:
var m map[string]int
if m == nil {
fmt.Println("Map is nil")
} else {
fmt.Println("Map is not nil")
}
不過切片變數和 map 變數在這裡也有些不同。初值為零值 nil 的切片型別變數,可以藉助內建的 append 的函式進行操作,這種在 Go 語言中被稱為“零值可用”。定義“零值可用”的型別,可以提升我們開發者的使用體驗,我們不用再擔心變數的初始狀態是否有效。比如,建立一個儲存字串到整數的對映,但 map 型別,因為它內部實現的複雜性,無法“零值可用”。所以,如果我們對處於零值狀態的 map 變數直接進行操作,就會導致執行時異常(panic),從而導致程式程式異常退出:
var m map[string]int // m = nil
m["key"] = 1 // 發生執行時異常:panic: assignment to entry in nil map
所以,我們必須對 map 型別變數進行顯式初始化後才能使用。我們先來看這句程式碼:
m := map[int]string{}
這裡,我們顯式初始化了 map 型別變數 m。不過,你要注意,雖然此時 map 型別變數 m 中沒有任何鍵值對,但變數 m 也不等同於初值為 nil 的 map 變數。這個時候,我們對 m 進行鍵值對的插入操作,不會引發執行時異常。
這裡我們再看看怎麼透過稍微複雜一些的複合字面值,對 map 型別變數進行初始化:
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 的顯式初始化,但不知道你有沒有發現一個問題,作為初值的字面值似乎有些“臃腫”。你看,作為初值的字面值採用了複合型別的元素型別,而且在編寫字面值時還帶上了各自的元素型別,比如作為 map[int] []string
值型別的[]string
,以及作為 map[Position]string
的 key 型別的 Position。
別急!針對這種情況,Go 提供了“語法糖”。這種情況下,Go 允許省略字面值中的元素型別。因為 map 型別表示中包含了 key 和 value 的元素型別,Go 編譯器已經有足夠的資訊,來推匯出字面值中各個值的型別了。我們以 m2 為例,這裡的顯式初始化程式碼和上面變數 m2 的初始化程式碼是等價的:
m2 := map[Position]string{
{29.935523, 52.568915}: "school",
{25.352594, 113.304361}: "shopping-mall",
{73.224455, 111.804306}: "hospital",
}
綜上,這種方式通常用於建立具有初始值的map
。在這種情況下,不需要使用make
函式。map
的宣告方式如下:
// 使用字面量宣告和初始化 map
myMap := map[keyType]valueType{
key1: value1,
key2: value2,
// ...
}
其中:
keyType
是鍵的型別valueType
是值的型別- 然後使用大括號
{}
包圍鍵值對
三.map 變數的傳遞開銷(map是引用傳遞)
和切片型別一樣,map 也是引用型別。這就意味著 map 型別變數作為引數被傳遞給函式或方法的時候,實質上傳遞的只是一個“描述符”,而不是整個 map 的資料複製,所以這個傳遞的開銷是固定的,而且也很小。
並且,當 map 變數被傳遞到函式或方法內部後,我們在函式內部對 map 型別引數的修改在函式外部也是可見的。比如你從這個示例中就可以看到,函式 foo 中對 map 型別變數 m 進行了修改,而這些修改在 foo 函式外也可見。
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 引用型別。當 map 被賦值為一個新變數的時候,它們指向同一個內部資料結構。因此,當改變其中一個變數,就會影響到另一變數。
四.map 的內部實現
4.1 map 型別在 Go 執行時層實現的示意圖
和切片相比,map 型別的內部實現要更加複雜。Go 執行時使用一張雜湊表來實現抽象的 map 型別。執行時實現了 map 型別操作的所有功能,包括查詢、插入、刪除等。在編譯階段,Go 編譯器會將 Go 語法層面的 map 操作,重寫成執行時對應的函式呼叫。大致的對應關係是這樣的:
// 建立map型別變數例項
m := make(map[keyType]valType, capacityhint) → m := runtime.makemap(maptype, capacityhint, m)
// 插入新鍵值對或給鍵重新賦值
m["key"] = "value" → v := runtime.mapassign(maptype, m, "key") v是用於後續儲存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 執行時層實現的示意圖:
我們可以看到,和切片的執行時表示圖相比,map 的實現示意圖顯然要複雜得多。接下來,我們結合這張圖來簡要描述一下 map 在執行時層的實現原理。接下來我們來看一下一個 map 變數在初始狀態、進行鍵值對操作後,以及在併發場景下的 Go 執行時層的實現原理。
4.2 初始狀態
從圖中我們可以看到,與語法層面 map 型別變數(m)一一對應的是 *runtime.hmap
的例項,即 runtime.hmap
型別的指標,也就是我們前面在講解 map 型別變數傳遞開銷時提到的 map 型別的描述符。hmap 型別是 map 型別的頭部結構(header
),它儲存了後續 map
型別操作所需的所有資訊,包括:
真正用來儲存鍵值對資料的是桶,也就是 bucket,每個 bucket 中儲存的是 Hash 值低 bit 位數值相同的元素,預設的元素個數為 BUCKETSIZE(值為 8,Go 1.17 版本中在 $GOROOT/src/cmd/compile/internal/reflectdata/reflect.go
中定義,與 runtime/map.go
中常量 bucketCnt
保持一致)。
當某個 bucket(比如 buckets[0]) 的 8 個空槽 slot)都填滿了,且 map 尚未達到擴容的條件的情況下,執行時會建立 overflow bucket,並將這個 overflow bucket 掛在上面 bucket(如 buckets[0])末尾的 overflow 指標上,這樣兩個 buckets 形成了一個連結串列結構,直到下一次 map 擴容之前,這個結構都會一直存在。
從圖中我們可以看到,每個 bucket 由三部分組成,從上到下分別是 tophash 區域、key 儲存區域和 value 儲存區域。
4.3 tophash 區域
當我們向 map
插入一條資料,或者是從 map
按 key
查詢資料的時候,執行時都會使用雜湊函式對 key
做雜湊運算,並獲得一個雜湊值(hashcode)
。這個 hashcode
非常關鍵,執行時會把 hashcode
“一分為二”來看待,其中低位區的值用於選定 bucket
,高位區的值用於在某個 bucket
中確定 key
的位置。我把這一過程整理成了下面這張示意圖,你理解起來可以更直觀:
因此,每個 bucket 的 tophash 區域其實是用來快速定位 key 位置的,這樣就避免了逐個 key 進行比較這種代價較大的操作。尤其是當 key 是 size 較大的字串型別時,好處就更突出了。這是一種以空間換時間的思路。
4.4 key 儲存區域
接著,我們看 tophash 區域下面是一塊連續的記憶體區域,儲存的是這個 bucket 承載的所有 key 資料。執行時在分配 bucket 的時候需要知道 key 的 Size。那麼執行時是如何知道 key 的 size 的呢?
當我們宣告一個 map 型別變數,比如 var m map[string]int
時,Go 執行時就會為這個變數對應的特定 map 型別,生成一個 runtime.maptype
例項。如果這個例項已經存在,就會直接複用。maptype 例項的結構是這樣的:
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
}
我們可以看到,這個例項包含了我們需要的 map 型別中的所有"元資訊"。我們前面提到過,編譯器會把語法層面的 map 操作重寫成執行時對應的函式呼叫,這些執行時函式都有一個共同的特點,那就是第一個引數都是 maptype 指標型別的引數。
Go 執行時就是利用 maptype 引數中的資訊確定 key 的型別和大小的。map
所用的 hash 函式也存放在 maptype.key.alg.hash(key, hmap.hash0)
中。同時 maptype 的存在也讓 Go 中所有 map 型別都共享一套執行時 map 操作函式,而不是像 C++
那樣為每種 map
型別建立一套 map
操作函式,這樣就節省了對最終二進位制檔案空間的佔用。
4.5 value 儲存區域
我們再接著看 key 儲存區域下方的另外一塊連續的記憶體區域,這個區域儲存的是 key 對應的 value
。和 key
一樣,這個區域的建立也是得到了 maptype
中資訊的幫助。Go 執行時採用了把 key
和 value
分開儲存的方式,而不是採用一個 kv
接著一個 kv
的 kv
緊鄰方式儲存,這帶來的其實是演算法上的複雜性,但卻減少了因記憶體對齊帶來的記憶體浪費。
我們以 map[int8]int64
為例,看看下面的儲存空間利用率對比圖:
你會看到,當前 Go 執行時使用的方案記憶體利用效率很高,而 kv 緊鄰儲存的方案在 map[int8]int64
這樣的例子中記憶體浪費十分嚴重,它的記憶體利用率是 72/128=56.25%,有近一半的空間都浪費掉了。
另外,還有一點我要跟你強調一下,如果 key 或 value 的資料長度大於一定數值,那麼執行時不會在 bucket 中直接儲存資料,而是會儲存 key 或 value 資料的指標。目前 Go 執行時定義的最大 key 和 value 的長度是這樣的:
// $GOROOT/src/runtime/map.go
const (
maxKeySize = 128
maxElemSize = 128
)
五.map 擴容
我們前面提到過,map 會對底層使用的記憶體進行自動管理。因此,在使用過程中,當插入元素個數超出一定數值後,map 一定會存在自動擴容的問題,也就是怎麼擴充 bucket 的數量,並重新在 bucket 間均衡分配資料的問題。
那麼 map 在什麼情況下會進行擴容呢?Go 執行時的 map 實現中引入了一個 LoadFactor
(負載因子),當 count > LoadFactor * 2^B 或 overflow bucket
過多時,執行時會自動對 map 進行擴容。目前 Go 1.17 版本 LoadFactor
設定為 6.5(loadFactorNum/loadFactorDen)
。這裡是 Go 中與 map 擴容相關的部分原始碼:
// $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
}
... ...
}
這兩方面原因導致的擴容,在執行時的操作其實是不一樣的。如果是因為 overflow bucket 過多導致的“擴容”,實際上執行時會新建一個和現有規模一樣的 bucket 陣列,然後在 assign 和 delete 時做排空和遷移。
如果是因為當前資料數量超出 LoadFactor 指定水位而進行的擴容,那麼執行時會建立一個兩倍於現有規模的 bucket 陣列,但真正的排空和遷移工作也是在 assign 和 delete 時逐步進行的。原 bucket 陣列會掛在 hmap 的 oldbuckets 指標下面,直到原 buckets 陣列中所有資料都遷移到新陣列後,原 buckets 陣列才會被釋放。你可以結合下面的 map 擴容示意圖來理解這個過程,這會讓你理解得更深刻一些:
六.map 與併發
接著我們來看一下 map 和併發。從上面的實現原理來看,充當 map 描述符角色的 hmap 例項自身是有狀態的(hmap.flags),而且對狀態的讀寫是沒有併發保護的。所以說 map 例項不是併發寫安全的,也不支援併發讀寫。如果我們對 map 例項進行併發讀寫,程式執行時就會丟擲異常。你可以看看下面這個併發讀寫 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)
}
執行這個示例程式,我們會得到下面的執行錯誤結果:
fatal error: concurrent map iteration and map write
不過,如果我們僅僅是進行併發讀,map 是沒有問題的。而且,Go 1.9 版本中引入了支援併發寫安全的 sync.Map 型別,可以在併發讀寫的場景下替換掉 map。如果你有這方面的需求,可以檢視一下sync.Map 的手冊。
另外,你要注意,考慮到 map 可以自動擴容,map 中資料元素的 value 位置可能在這一過程中發生變化,所以 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 中對應的 key 就可以了:
// 建立並初始化一個 map
myMap := make(map[string]int)
myMap["apple"] = 1
myMap["banana"] = 2
不需要自己判斷資料有沒有插入成功,因為 Go 會保證插入總是成功的。不過,如果我們插入新鍵值對的時候,某個 key 已經存在於 map 中了,那我們的插入操作就會用新值覆蓋舊值:
// 修改鍵 "apple" 對應的值
myMap["apple"] = 3
// 更新鍵 "cherry" 對應的值,如果鍵不存在則建立新鍵值對
myMap["cherry"] = 4
// 列印修改後的 map
fmt.Println(myMap) // 輸出: map[apple:3 banana:2 cherry:4]
從這段程式碼中,您可以看到如何執行以下操作:
- 修改鍵 "apple" 對應的值:使用
myMap["apple"] = 3
這行程式碼,將鍵 "apple" 對應的值從原來的 1 修改為 3。 - 更新鍵 "cherry" 對應的值:使用
myMap["cherry"] = 4
這行程式碼,更新了鍵 "cherry" 對應的值為 4。如果鍵 "cherry" 不存在於map
中,這行程式碼會建立一個新的鍵值對。 - 列印修改後的 map:最後使用
fmt.Println(myMap)
列印整個修改後的map
,以顯示更新後的鍵值對。
7.2 批次更新和修改(合併同型別map)
在Go中,可以使用迴圈遍歷另一個map
,然後使用遍歷的鍵值對來批次更新或修改目標map
的鍵值對。以下是一個實現類似於Python字典的update()
方法的步驟:
- 建立一個目標
map
,它將被更新或修改。 - 建立一個源
map
,其中包含要合併到目標map
的鍵值對。 - 遍歷源
map
的鍵值對。 - 對於每個鍵值對,檢查它是否存在於目標
map
中。- 如果存在,將目標
map
中的值更新為源map
中的值。 - 如果不存在,將源
map
中的鍵值對新增到目標map
中。
- 如果存在,將目標
- 最終,目標
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() {
// 建立目標 map
targetMap := map[string]int{
"apple": 1,
"banana": 2,
}
// 建立源 map,包含要更新或修改的鍵值對
sourceMap := map[string]int{
"apple": 3, // 更新 "apple" 的值為 3
"cherry": 4, // 新增新的鍵值對 "cherry": 4
}
// 呼叫 updateMap 函式,將源 map 合併到目標 map 中
updateMap(targetMap, sourceMap)
// 列印更新後的目標 map
fmt.Println(targetMap) // 輸出:map[apple:3 banana:2 cherry:4]
}
7.3 獲取鍵值對數量
要獲取一個map
中鍵值對的數量(也稱為長度),可以使用Go語言的len
函式。len
函式返回map
中鍵值對的數量。以下是獲取map
中鍵值對數量的示例:
// 建立並初始化一個 map
myMap := map[string]int{
"apple": 1,
"banana": 2,
"cherry": 3,
}
// 使用 len 函式獲取 map 的鍵值對數量
count := len(myMap)
// 列印鍵值對數量
fmt.Println("鍵值對數量:", count)
不過,這裡要注意的是我們不能對 map 型別變數呼叫 cap,來獲取當前容量,這是 map 型別與切片型別的一個不同點。
7.4 查詢和資料讀取(判斷某個鍵是否存在)
7.4.1 查詢和資料讀取 map 語法格式
Go語言中有個判斷map中鍵是否存在的特殊寫法,格式如下:
value, ok := map[key]
其中:
myMap
是目標map
,您希望在其中查詢鍵。key
是您要查詢的鍵。value
是一個變數,如果鍵存在,它將儲存鍵對應的值,如果鍵不存在,則會獲得值型別的零值。ok
是一個布林值,用於指示鍵是否存在。如果鍵存在,ok
為true
;如果鍵不存在,ok
為false
。
map 型別更多用在查詢和資料讀取場合。所謂查詢,就是判斷某個 key 是否存在於某個 map 中。Go 語言的 map 型別支援透過用一種名為“comma ok”的慣用法,進行對某個 key 的查詢。接下來我們就用“comma ok”慣用法改造一下上面的程式碼:
m := make(map[string]int)
v, ok := m["key1"]
if !ok {
// "key1"不在map中
}
// "key1"在map中,v將被賦予"key1"鍵對應的value
我們看到,這裡我們透過了一個布林型別變數 ok,來判斷鍵“key1”是否存在於 map 中。如果存在,變數 v 就會被正確地賦值為鍵“key1”對應的 value。
不過,如果我們並不關心某個鍵對應的 value,而只關心某個鍵是否在於 map 中,我們可以使用空識別符號替代變數 v,忽略可能返回的 value:
m := make(map[string]int)
_, ok := m["key1"]
... ...
因此,你一定要記住:在 Go 語言中,請使用“comma ok”慣用法對 map 進行鍵查詢和鍵值讀取操作。
7.4.2 實現get 方法查詢map 對應的key
在Go中,要實現類似Python字典的get()
方法,可以編寫一個函式,該函式接受一個map
、一個鍵以及一個預設值作為引數。函式將嘗試從map
中獲取指定鍵的值,如果鍵不存在,則返回預設值。以下是實現類似get()
方法的步驟:
- 建立一個函式,命名為
get
,該函式接受三個引數:map
、鍵和預設值。 - 在函式中,使用鍵來嘗試從
map
中獲取對應的值。 - 如果值存在,返回該值;如果不存在,則返回預設值空字串。
package main
import (
"fmt"
)
// 實現類似 Python 字典的 get() 方法
func get(myMap map[string]string, key string) string {
value, ok := myMap[key]
if !ok {
return ""
}
return value
}
func main() {
// 建立並初始化一個 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的對應的值!")
} else {
fmt.Println("Color of 'tangerine':", grapeValue)
}
}
執行此程式碼將輸出:
Color of 'apple': red
沒有獲取到tangerine的對應的值!
7.5 使用delete()函式刪除鍵值對
使用delete()
內建函式從map中刪除一組鍵值對,delete()
函式的格式如下:
delete(map, key)
其中:
- map:表示要刪除鍵值對的map
- key:表示要刪除的鍵值對的鍵
使用 delete 函式的情況下,傳入的第一個引數是我們的 map 型別變數,第二個引數就是我們想要刪除的鍵。我們可以看看這個程式碼示例:
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 中的鍵值資料
最後,我們來說一下如何遍歷 map 中的鍵值資料。這一點雖然不像查詢和讀取操作那麼常見,但日常開發中我們還是有這個需求的。在 Go 中,遍歷 map 的鍵值對只有一種方法,那就是像對待切片那樣透過 for range 語句對 map 資料進行遍歷。我們看一個例子:
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 中,它對應的值儲存在變數 v 中。我們可以執行一下這段程式碼,可以得到符合我們預期的結果:
{ [1, 11] [2, 12] [3, 13] }
如果我們只關心每次迭代的鍵,我們可以使用下面的方式對 map 進行遍歷:
for k, _ := range m {
// 使用k
}
當然更地道的方式是這樣的:
for k := range m {
// 使用k
}
如果我們只關心每次迭代返回的鍵所對應的 value,我們同樣可以透過空識別符號替代變數 k,就像下面這樣:
for _, v := range m {
// 使用v
}
不過,前面 map 遍歷的輸出結果都非常理想,給我們的表象好像是迭代器按照 map 中元素的插入次序逐一遍歷。那事實是不是這樣呢?我們再來試試,多遍歷幾次這個 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)
}
}
執行一下上述程式碼,我們可以得到這樣結果:
{ [1, 11] [2, 12] [3, 13] }
{ [2, 12] [3, 13] [1, 11] }
{ [1, 11] [2, 12] [3, 13] }
我們可以看到,對同一 map 做多次遍歷的時候,每次遍歷元素的次序都不相同。這是 Go 語言 map 型別的一個重要特點,也是很容易讓 Go 初學者掉入坑中的一個地方。所以這裡你一定要記住:程式邏輯千萬不要依賴遍歷 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 { // 直接報錯,不能直接比較
}
}