Key的選擇注意點(diǎn)
key 類型的 K 必須是可比較的(comparable),也就是可以通過 == 和 != 操作符進(jìn)行比較;value 的值和類型無所謂,可以是任意的類型,或者為 nil。
在 Go 語言中,bool、整數(shù)、浮點(diǎn)數(shù)、復(fù)數(shù)、字符串、指針、Channel、接口都是可比較的,包含可比較元素的 struct 和數(shù)組,這倆也是可比較的,而 slice、map、函數(shù)值都是不可比較的。
通常情況下,我們會(huì)選擇內(nèi)建的基本類型,比如整數(shù)、字符串做 key 的類型,因?yàn)檫@樣最方便。
如果使用 struct 類型做 key 其實(shí)是有坑的,因?yàn)槿绻?struct 的某個(gè)字段值修改了,查詢 map 時(shí)無法獲取它 add 進(jìn)去的值。
如果要使用 struct 作為 key,我們要保證 struct 對(duì)象在邏輯上是不可變的,這樣才會(huì)保證 map 的邏輯沒有問題。
Value獲取注意點(diǎn)
在 Go 中,map[key]函數(shù)返回結(jié)果可以是一個(gè)值,也可以是兩個(gè)值,這是容易讓人迷惑的地方。原因在于,如果獲取一個(gè)不存在的 key 對(duì)應(yīng)的值時(shí),會(huì)返回零值。為了區(qū)分真正的零值和 key 不存在這兩種情況,可以根據(jù)第二個(gè)返回值來區(qū)分,如下面的代碼的第 6 行、第 7 行:
func main() {
var m = make(map[string]int)
m["a"] = 0
fmt.Printf("a=%d; b=%d\n", m["a"], m["b"])
av, aexisted := m["a"]
bv, bexisted := m["b"]
fmt.Printf("a=%d, existed: %t; b=%d, existed: %t\n", av, aexisted, bv, bexisted)
}
遍歷
map 是無序的,所以當(dāng)遍歷一個(gè) map 對(duì)象的時(shí)候,迭代的元素的順序是不確定的,無法保證兩次遍歷的順序是一樣的,也不能保證和插入的順序一致。那怎么辦呢?如果我們想要按照 key 的順序獲取 map 的值,需要先取出所有的 key 進(jìn)行排序,然后按照這個(gè)排序的 key 依次獲取對(duì)應(yīng)的值。
常見錯(cuò)誤
常見錯(cuò)誤一:未初始化
和 slice 或者 Mutex、RWmutex 等 struct 類型不同,map 對(duì)象必須在使用之前初始化。如果不初始化就直接賦值的話,會(huì)出現(xiàn) panic 異常。
從一個(gè) nil 的 map 對(duì)象中獲取值不會(huì) panic,而是會(huì)得到零值。
常見錯(cuò)誤二:并發(fā)讀寫
對(duì)于 map 類型,另一個(gè)很容易犯的錯(cuò)誤就是并發(fā)訪問問題,程序在運(yùn)行的時(shí)候就有可能出現(xiàn)并發(fā)讀寫導(dǎo)致的 panic。
Go 內(nèi)建的 map 對(duì)象不是線程(goroutine)安全的,并發(fā)讀寫的時(shí)候運(yùn)行時(shí)會(huì)有檢查,遇到并發(fā)問題就會(huì)導(dǎo)致 panic。 如果map需要支持并發(fā)讀寫,可以自行實(shí)現(xiàn)并發(fā)讀寫安全的map或者使用sync.Map。
實(shí)現(xiàn)線程安全的 map
加讀寫鎖:擴(kuò)展 map,支持并發(fā)讀寫
type RWMap struct { // 一個(gè)讀寫鎖保護(hù)的線程安全的map
sync.RWMutex // 讀寫鎖保護(hù)下面的map字段
m map[int]int
}
// 新建一個(gè)RWMap
func NewRWMap(n int) *RWMap {
return &RWMap{
m: make(map[int]int, n),
}
}
func (m *RWMap) Get(k int) (int, bool) { //從map中讀取一個(gè)值
m.RLock()
defer m.RUnlock()
v, existed := m.m[k] // 在鎖的保護(hù)下從map中讀取
return v, existed
}
func (m *RWMap) Set(k int, v int) { // 設(shè)置一個(gè)鍵值對(duì)
m.Lock() // 鎖保護(hù)
defer m.Unlock()
m.m[k] = v
}
func (m *RWMap) Delete(k int) { //刪除一個(gè)鍵
m.Lock() // 鎖保護(hù)
defer m.Unlock()
delete(m.m, k)
}
func (m *RWMap) Len() int { // map的長(zhǎng)度
m.RLock() // 鎖保護(hù)
defer m.RUnlock()
return len(m.m)
}
func (m *RWMap) Each(f func(k, v int) bool) { // 遍歷map
m.RLock() //遍歷期間一直持有讀鎖
defer m.RUnlock()
for k, v := range m.m {
if !f(k, v) {
return
}
}
}
正如這段代碼所示,對(duì) map 對(duì)象的操作,無非就是增刪改查和遍歷等幾種常見操作。我們可以把這些操作分為讀和寫兩類,其中,查詢和遍歷可以看做讀操作,增加、修改和刪除可以看做寫操作。如例子所示,我們可以通過讀寫鎖對(duì)相應(yīng)的操作進(jìn)行保護(hù)。
分片加鎖:更高效的并發(fā) map
雖然使用讀寫鎖可以提供線程安全的 map,但是在大量并發(fā)讀寫的情況下,鎖的競(jìng)爭(zhēng)會(huì)非常激烈。
在并發(fā)編程中,我們的一條原則就是盡量減少鎖的使用。一些單線程單進(jìn)程的應(yīng)用(比如 Redis 等),基本上不需要使用鎖去解決并發(fā)線程訪問的問題,所以可以取得很高的性能。但是對(duì)于 Go 開發(fā)的應(yīng)用程序來說,并發(fā)是常用的一個(gè)特性,在這種情況下,我們能做的就是,盡量減少鎖的粒度和鎖的持有時(shí)間。
減少鎖的粒度常用的方法就是分片(Shard),將一把鎖分成幾把鎖,每個(gè)鎖控制一個(gè)分片。Go 比較知名的分片并發(fā) map 的實(shí)現(xiàn)是 orcaman/concurrent-map。
它默認(rèn)采用 32 個(gè)分片,GetShard 是一個(gè)關(guān)鍵的方法,能夠根據(jù) key 計(jì)算出分片索引。
var SHARD_COUNT = 32
// 分成SHARD_COUNT個(gè)分片的map
type ConcurrentMap []*ConcurrentMapShared
// 通過RWMutex保護(hù)的線程安全的分片,包含一個(gè)map
type ConcurrentMapShared struct {
items map[string]interface{}
sync.RWMutex // Read Write mutex, guards access to internal map.
}
// 創(chuàng)建并發(fā)map
func New() ConcurrentMap {
m := make(ConcurrentMap, SHARD_COUNT)
for i := 0; i < SHARD_COUNT; i++ {
m[i] = &ConcurrentMapShared{items: make(map[string]interface{})}
}
return m
}
// 根據(jù)key計(jì)算分片索引
func (m ConcurrentMap) GetShard(key string) *ConcurrentMapShared {
return m[uint(fnv32(key))%uint(SHARD_COUNT)]
}
增加或者查詢的時(shí)候,首先根據(jù)分片索引得到分片對(duì)象,然后對(duì)分片對(duì)象加鎖進(jìn)行操作:
func (m ConcurrentMap) Set(key string, value interface{}) {
// 根據(jù)key計(jì)算出對(duì)應(yīng)的分片
shard := m.GetShard(key)
shard.Lock() //對(duì)這個(gè)分片加鎖,執(zhí)行業(yè)務(wù)操作
shard.items[key] = value
shard.Unlock()
}
func (m ConcurrentMap) Get(key string) (interface{}, bool) {
// 根據(jù)key計(jì)算出對(duì)應(yīng)的分片
shard := m.GetShard(key)
shard.RLock()
// 從這個(gè)分片讀取key的值
val, ok := shard.items[key]
shard.RUnlock()
return val, ok
}
加鎖和分片加鎖這兩種方案都比較常用,如果是追求更高的性能,顯然是分片加鎖更好,因?yàn)樗梢越档玩i的粒度,進(jìn)而提高訪問此 map 對(duì)象的吞吐。如果并發(fā)性能要求不是那么高的場(chǎng)景,簡(jiǎn)單加鎖方式更簡(jiǎn)單。