業余時間翻譯,水平很差,如有瑕疵,純屬無能。
原文鏈接
http://blog.golang.org/go-maps-in-action
go語言中的map實戰
1. 簡介
哈希表是計算機科學中最重要的數據結構之一。許多哈希表的實現有着千差萬別的特性,但是總體上他們都提供了快速查詢,添加和刪除功能。go語言提供了內置數據類型map。
2. 聲明和初始化
map的聲明格式如下:
map[KeyType] ValueType
KeyType類型必須是可以比較的,而ValueType可以是任意類型,甚至是另一個map。
以下這個m是一個鍵值為string,值為int的哈希表:
var m map[string]int
哈希表類型是引用類型,像指針或者切片m指向的值是nil;它沒有指向一個初始化了的哈希表。一個nil哈希表在讀的時候,像一個空的哈希表,但是嘗試向m中寫數據會引發一個運行時panic,所以別那樣做。 使用內置函數make初始化一個哈希表:
m = make(map[string]int)
make函數申請並初始化了一個哈希表的數據結構並且返回一個指向這個初始化好了的哈希表。 哈希表的數據結構是go本身運行時的一個實現細節,並沒有被語言本身所規定【翻者補充:類似c++不同編譯器如何實現虛函數一樣吧】。 文章中只關心哈希表的使用而非實現。
3. 使用哈希表
go中的哈希表的使用方法和其他語言相似,向哈希表中插入一個鍵為“route”值為66的語句為:
m["route"] = 66
查詢鍵為“route”並且把對應的值賦給新的變量i的語句為:
i := m["route"]
如果查詢的鍵值在哈希表中不存在,將拿到值類型的“0”值。 以m為例,值類型為int,則“0”值為1:
j := m["root"] // j == 0
len是返回哈希表中數據個數的內置函數:
n := len(m)
delete是刪除哈希表中某一鍵值數據的內置函數:
delete(m, "route")
delete函數返回值為空,如果鍵值不存在則不做任何操作。
使用兩個返回值的方式可以檢查鍵值是否存在:
i, ok := m["route"]
在這個語句中,i被賦值為哈希表中鍵值為“route”的值。如果那個鍵值不存在,i被賦值為值類型中的“0”值。第二個返回值是布爾類型,如果是true,表明鍵值存在,否則不存在。
如果只是檢查鍵值是否存在,則第一個返回值使用下划線“_":
_, 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{}
4. 利用“0”值
在哈希表中查詢數據,如果鍵值不存在,返回一個值類型的“0”值是很方便的:
It can be convenient that a map retrieval yields a zero value when the key is not present.
例如,一個布爾值的哈希表可以被用來當做一個set使用(布爾類型的“0”值是false)。下邊這個例子遍歷一個Node的鏈表並且打印他們的值,它使用了一個Node的指針為key的哈希表去判斷鏈表中是否有環:
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已經被訪問過了,如果是假,表明還沒有。 這樣就不需要使用兩個返回值的方式去檢查n是否在map中真的存在;默認的“0”值幫我們做了。
另一個有用的例子是切片的哈希表。想一個空切片中添加數據會申請一個新的切片所以想一個切片的map中append數據會只是占用一行;不需要檢查key是否存在。在下邊的例子中,切片用被用來存放person類型的值,每個Person有一個Name字段和一個切片,這個例子中創建了一個哈希表關聯每種物品和一個喜歡他的人的切片。【做了個倒排?】
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)
}
}
打印喜歡cheese的People:
for _, p := range likes["cheese"] {
fmt.Println(p.Name, "likes cheese.")
}
打印出喜歡bacon的人數
fmt.Println(len(likes["bacon"]), "people like bacon.")
注意,這里range函數和len函數都把nil切片看做一個長度為零的切片,即使沒有人喜歡cheese或者bacon,也不會有問題。
5. 鍵的類型
像剛才提過的,鍵的類型必須是可比較的。go語言的spec中准確的定義了這個要求,簡而言之,可以比較的類型包括:布爾,數字,字符串,指針,消息channel,接口類型和任何包含了以上類型的結構體和數組。不在此范圍的類型包括切片,哈希表和函數;這些類型不能使用 “==” 做比較,也不能被用來做哈希表的鍵值。
很明顯字符串,整型和其他基礎類型可以作為哈希表的鍵。 意想不到的是結構體也可以作為鍵值,例如,這個哈希表的哈希表可以用來存放不同國家的訪問數。
hits := make(map[string]map[string]int)
這是一個鍵為字符串,值為字符串到int的哈希表。最外邊表的每一個鍵值是到達內部哈希表的路徑,每個被嵌套的哈希表的鍵值是一個兩個字母的國家碼。這個表達式可以得到一個澳大利亞人訪問文檔頁面的次數。
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")
另一方面,使用結構體作為鍵的哈希表可以解除這種復雜性:
type Key struct {
Path, Country string
}
hits := make(map[Key]int)
當一個越南人訪問主頁的時候,一行就可以搞定:
hits[Key{"/", "vn"}]++
查看多少個瑞士人查看了spec頁面的語句也很簡單:
n := hits[Key{"/ref/spec", "ch"}]
6. 並發
哈希表在有並發的場景並不安全:同時讀寫一個哈希表的后果是不確定的。如果你需要使用goroutines同時對一個哈希表做讀寫,對哈希表的訪問需要通過某種同步機制做協調。一個常用的方法是是使用 sync.RWMutex。
這個語句生命了一個counter變量,這是一個包含了一個map和sync.RWMutex的匿名結構體。
var counter = struct{
sync.RWMutex
m map[string]int
}{m: make(map[string]int)}
讀counter前,獲取讀鎖:
counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)
寫counter前,獲取寫鎖
counter.Lock() counter.m["some_key"]++ counter.Unlock()
7. 遍歷順序
當使用range循環遍歷一個哈希表的時候,遍歷順序是不保證穩定的。因為Go1版本將map的便利順序隨機化了,如果程序依賴之前實現中的穩定的便利順序的話。【翻者注。。不知道怎么翻譯】 如果你需要一個穩定的遍歷順序,你必須維護一個獨立的數據結構用來保證這個順序。下面這個例子使用了一個獨立的排序切片,按照鍵值順序打印一個 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])
}
