Go 部落格
Go map in action
引言
計算機科學中最有用的資料結構之一是雜湊表。存在許多具有不同特性的雜湊表實現,但總的來說,它們提供了快速的查詢、新增和刪除。Go 提供了一個內建的 map 型別,它實現了雜湊表。
宣告和初始化
Go 的 map 型別如下所示
map[KeyType]ValueType
其中 KeyType
可以是任何 可比較 的型別(稍後詳細介紹),而 ValueType
可以是任何型別,包括另一個 map!
變數 m
是一個從 string 鍵對映到 int 值的 map
var m map[string]int
Map 型別是引用型別,類似於指標或 slice,因此上面 m
的值為 nil
;它不指向已初始化的 map。nil map 在讀取時表現得像一個空 map,但嘗試寫入 nil map 會導致執行時 panic;請勿這樣做。要初始化 map,請使用內建的 make
函式
m = make(map[string]int)
make
函式會分配並初始化一個雜湊 map 資料結構,並返回指向它的 map 值。該資料結構的具體實現是執行時的一個實現細節,語言本身並未指定。在本文中,我們將重點關注 map 的使用,而不是它們的實現。
使用 map
Go 提供了熟悉的 map 操作語法。此語句將鍵 "route"
設定為值 66
m["route"] = 66
此語句檢索鍵 "route"
下儲存的值,並將其賦給新變數 i
i := m["route"]
如果請求的鍵不存在,我們將得到值型別的零值。在本例中,值型別是 int
,因此零值是 0
j := m["root"]
// j == 0
內建的 len
函式返回 map 中條目的數量
n := len(m)
內建的 delete
函式會從 map 中刪除一個條目
delete(m, "route")
delete
函式不返回任何內容,如果指定的鍵不存在,則什麼也不做。
一個雙值賦值用於測試鍵是否存在
i, ok := m["route"]
在此語句中,第一個值(i
)被賦給鍵 "route"
下儲存的值。如果該鍵不存在,i
將是值型別的零值(0
)。第二個值(ok
)是一個 bool
,如果鍵存在於 map 中,則為 true
,否則為 false
。
要測試鍵是否存在而不檢索值,請在第一個值的位置使用下劃線 (_)
_, ok := m["route"]
要迭代 map 的內容,請使用 range
關鍵字
for key, value := range m {
fmt.Println("Key:", key, "Value:", value)
}
要用一些資料初始化 map,請使用 map 字面量
commits := map[string]int{
"rsc": 3711,
"r": 2138,
"gri": 1908,
"adg": 912,
}
相同的語法可用於初始化空 map,這在功能上與使用 make
函式相同
m = map[string]int{}
利用零值
當鍵不存在時,map 檢索返回零值可能很方便。
例如,布林值 map 可以用作類似集合的資料結構(回想一下布林型別的零值是 false)。此示例遍歷 Node
的連結串列並列印其值。它使用 Node
指標的 map 來檢測列表中的迴圈。
type Node struct { Next *Node Value interface{} } var first *Node visited := make(map[*Node]bool) for n := first; n != nil; n = n.Next { if visited[n] { fmt.Println("cycle detected") break } visited[n] = true fmt.Println(n.Value) }
表示式 visited[n]
如果 n
已被訪問,則為 true
,如果 n
不存在,則為 false
。無需使用雙值形式來測試 n
在 map 中是否存在;零值預設值可以幫我們完成。
另一個有用的零值示例是 slice 的 map。追加到 nil slice 會分配一個新的 slice,因此將值追加到 slice 的 map 只需要一行程式碼;無需檢查鍵是否存在。在以下示例中,slice people 被 Person
值填充。每個 Person
都有一個 Name
和一個 Likes slice。該示例建立了一個 map,將每個 like 與喜歡它的人的 slice 關聯起來。
type Person struct { Name string Likes []string } var people []*Person likes := make(map[string][]*Person) for _, p := range people { for _, l := range p.Likes { likes[l] = append(likes[l], p) } }
列印喜歡乳酪的人的列表
for _, p := range likes["cheese"] { fmt.Println(p.Name, "likes cheese.") }
列印喜歡培根的人的數量
fmt.Println(len(likes["bacon"]), "people like bacon.")
請注意,由於 range 和 len 都將 nil slice 視為零長度 slice,因此最後兩個示例即使沒有人喜歡乳酪或培根(儘管這可能不太可能)也能正常工作。
鍵型別
如前所述,map 鍵可以是任何可比較的型別。 語言規範精確地定義了這一點,但簡而言之,可比較的型別是布林型、數值型、字串型、指標型、通道型和介面型,以及僅包含這些型別的結構體或陣列。列表中值得注意的是 slice、map 和函式;這些型別不能使用 ==
進行比較,也不能用作 map 鍵。
顯而易見,字串、整數和其他基本型別應該可以作為 map 鍵,但可能出乎意料的是結構體鍵。結構體可用於按多個維度對資料進行鍵控。例如,這個 map 的 map 可用於按國家統計網頁訪問量
hits := make(map[string]map[string]int)
這是從 string 到(從 string
到 int
的 map)的 map。外層 map 的每個鍵都是一個網頁的路徑,其中包含自己的內層 map。每個內層 map 的鍵是一個兩位數的國家程式碼。此表示式檢索澳大利亞人載入文件頁面的次數
n := hits["/doc/"]["au"]
不幸的是,這種方法在新增資料時會變得笨拙,因為對於任何給定的外層鍵,您都必須檢查內層 map 是否存在,並在需要時建立它
func add(m map[string]map[string]int, path, country string) {
mm, ok := m[path]
if !ok {
mm = make(map[string]int)
m[path] = mm
}
mm[country]++
}
add(hits, "/doc/", "au")
另一方面,使用具有結構體鍵的單個 map 的設計消除了所有這些複雜性
type Key struct {
Path, Country string
}
hits := make(map[Key]int)
當一個越南人訪問主頁時,增加(可能建立)相應的計數器只需一行程式碼
hits[Key{"/", "vn"}]++
同樣,很容易看到有多少瑞士人閱讀了規範
n := hits[Key{"/ref/spec", "ch"}]
併發
Map 不適用於併發訪問:同時讀寫它們會發生什麼是不確定的。如果您需要從併發執行的 goroutine 中讀取和寫入 map,則必須透過某種同步機制來協調訪問。保護 map 的一種常見方法是使用 sync.RWMutex。
此語句聲明瞭一個 counter
變數,它是一個匿名結構體,包含一個 map 和一個嵌入的 sync.RWMutex
。
var counter = struct{
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
要從計數器讀取,請獲取讀鎖
counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)
要向計數器寫入,請獲取寫鎖
counter.Lock()
counter.m["some_key"]++
counter.Unlock()
迭代順序
當使用 range 迴圈迭代 map 時,迭代順序未指定,並且不能保證每次迭代都相同。如果您需要穩定的迭代順序,則必須維護一個單獨的資料結構來指定該順序。此示例使用一個單獨的已排序鍵 slice 來按鍵順序列印 map[int]string
import "sort"
var m map[int]string
var keys []int
for k := range m {
keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
fmt.Println("Key:", k, "Value:", m[k])
}
下一篇文章:去 Go 會議
上一篇文章:格式化你的 Go 程式碼
部落格索引