Go 部落格

Go map in action

Andrew Gerrand
2013 年 2 月 6 日

引言

計算機科學中最有用的資料結構之一是雜湊表。存在許多具有不同特性的雜湊表實現,但總的來說,它們提供了快速的查詢、新增和刪除。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 到(從 stringint 的 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 程式碼
部落格索引