Go 部落格

Go 地圖實務操作

安德魯·格蘭德
2013 年 2 月 6 日

引言

電腦科學中最有用的資料結構之一是雜湊表。存在許多具有不同屬性的雜湊表實作,但一般而言它們提供快速的查詢、新增和刪除功能。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 整理程式碼
部落格索引