Go 部落格
Go 地圖實務操作
引言
電腦科學中最有用的資料結構之一是雜湊表。存在許多具有不同屬性的雜湊表實作,但一般而言它們提供快速的查詢、新增和刪除功能。Go 提供內建的 map 型別來實作雜湊表。
宣告和初始化
Go map 型別如下所示
map[KeyType]ValueType
其中 KeyType
可以是任何可進行 比較 的型別(後續會進一步說明),而 ValueType
可以是任何型別,包括其他 map!
此變數 m
的字串為鍵值、整數為值的 map
var m map[string]int
Map 型別為參考型別,例如指標或切片,所以上方 m
的值為 nil
;它並不指向已初始化的 map。在讀取時,nil map 會像空 map 一樣運作,但在嘗試寫入 nil map 時會導致執行時間發生錯誤;請勿執行此動作。若要初始化 map,請使用內建的 make
函式
m = make(map[string]int)
make
函式會配置和初始化雜湊表資料結構,並傳回指向它的 map 值。該資料結構的詳細資訊為執行時間的實作細節,而未由語言本身指定。在本文中,我們將專注於 map 的使用方式,而非其實作。
使用地圖
Go 提供一種用於處理映射的熟悉語法。此敘述將鍵值 "route"
設為值 66
m["route"] = 66
此敘述擷取儲存在鍵值 "route"
下的值,並將其指派給新變數 i
i := m["route"]
如果要求的鍵值不存在,我們就會取得該值類型的零值。在此範例中,值類型為 int
,因此零值為 0
j := m["root"]
// j == 0
內建的 len
函式會根據映射中的項目數目傳回
n := len(m)
內建的 delete
函式會從映射中移除一個項目
delete(m, "route")
delete
函式不會傳回任何內容,而且如果指定的鍵值不存在,便不會執行任何動作
二值指派會測試鍵值的存在性
i, ok := m["route"]
在此敘述中,第一個值 (i
) 指派了儲存在鍵值 "route"
下的值。如果該鍵值不存在,i
則為值類型的零值 (0
)。第二個值 (ok
) 為一個 bool
,如果鍵值存在於映射中,則為 true
,否則為 false
。
若要測試鍵值而無須擷取該值,請使用底線取代第一個值
_, ok := m["route"]
若要逐一處理映射中的內容,請使用 range
關鍵字
for key, value := range m {
fmt.Println("Key:", key, "Value:", value)
}
若要初始化附帶某些資料的映射,請使用映射文字
commits := map[string]int{
"rsc": 3711,
"r": 2138,
"gri": 1908,
"adg": 912,
}
相同的語法可用於初始化一個空的映射,其功能與使用 make
函式相同
m = map[string]int{}
使用零值
當鍵值不存在時,映射擷取會產生零值,而這相當便利。
舉例來說,布林值映射可用於類集合的資料結構 (請記得布林值類型的零值是 false)。此範例會橫跨 Nodes
的連結清單,並列印其值。它會使用指向 Node
的映射,用於偵測清單中的循環。
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
;零值預設值會幫我們完成此動作。
另一個有用的零值的範例是一個切片的映射。加到一個 nil 切片中只會分配一個新的切片,所以只要一行就可以在一個切片的映射中附加一個值;無需檢查鍵值是否存在。在以下範例中,people 切片已充滿了人員
值。每個人員
都有個名稱
及利益的切片。範例建立一個映射,來將每個喜好與一個喜歡它的切片相關聯。
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.")
要注意的是,由於範圍和長度都將空切片視為零長度切片,因此即使沒有人喜歡起司或培根,最後兩個範例還是會運作(但這不太可能)。
鍵值類型
如前所述,地圖鍵值可以使用任何可比較的類型。 語言規格 精確地定義了這一點,但簡而言之,可比較的類型包括布林值、數字、字串、指標、通道和介面類型,以及僅包含這些類型的結構或陣列。值得注意的是,清單中沒有切片、地圖和函式;這些類型無法使用 ==
進行比較,並且不能用作映射鍵。
很明顯,字串、整數和其他基本類型應該可用作地圖鍵,但可能意外的是結構鍵。結構可用於按多個維度對資料進行鍵控。例如,此地圖的映射可用於按國家/地區統計網頁瀏覽量
hits := make(map[string]map[string]int)
這是字串到(字串
到整數
的映射)的地圖。外部映射的每個鍵都是一個網頁的路徑,並有其自己的內部映射。每個內部映射鍵都是一個兩個字母的國家/地區代碼。此運算式會擷取澳洲人載入文件頁面的次數
n := hits["/doc/"]["au"]
不幸的是,此方法在新增資料時變得難以控制,因為對於任何既定的外部鍵,您都必須檢查內部映射是否存在,並在需要時建立它
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")
另一方面,使用具有結構鍵的單一映射的設計消除了所有這些複雜性
type Key struct {
Path, Country string
}
hits := make(map[Key]int)
當越南人拜訪首頁時,遞增(可能會建立)適當的計數器時,一行就夠了
hits[Key{"/", "vn"}]++
要找出有多少瑞士人閱讀過規格,同樣非常簡單
n := hits[Key{"/ref/spec", "ch"}]
並行
映射不可安全地並行使用:同時讀寫時會發生什麼行為是未定義的。如果您需要從同時執行 goroutine 的映射讀取和寫入,必須透過某種同步機制來調節存取。防護映射的一種常見方法是使用 sync.RWMutex。
此宣告陳述一個 counter
變數,它是包含映射和內嵌 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()
反覆運算順序
使用範圍迴圈反覆運算映射時,反覆運算順序未指定,且無法保證在不同反覆運算中會相同。如果您需要穩定的反覆運算順序,您必須維持一個單獨的資料結構,指定該順序。此範例使用另一個已排序的鍵片斷列來按鍵順序印出 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 聚會吧
上一篇:使用 gofmt 整理程式碼
部落格索引