Go - map

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)單。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • GO 中map的底層是如何實(shí)現(xiàn)的 首先Go 語言采用的是哈希查找表,并且使用鏈表解決哈希沖突。 GO的內(nèi)存模型 先...
    GGBond_8488閱讀 2,129評(píng)論 2 4
  • go map的線程安全使用 簡(jiǎn)單線程安全使用 在很多時(shí)候,我們會(huì)并發(fā)地使用map對(duì)象,尤其是在一定規(guī)模的項(xiàng)目中,m...
    吃貓的魚0閱讀 13,356評(píng)論 0 8
  • Q 怎么平滑的擴(kuò)容 沖突解決的2種方法 開放尋址法 開放尋址中對(duì)性能影響最大的計(jì)算裝載因子。 隨著裝載因子的怎額更...
    lucasgao閱讀 436評(píng)論 0 1
  • go map 比較深入的使用方案 參考blog: https://blog.golang.org/go-maps-...
    來福馬斯特閱讀 60,878評(píng)論 1 8
  • 目錄 統(tǒng)一規(guī)范篇 命名篇 開發(fā)篇 優(yōu)化篇 統(tǒng)一規(guī)范篇 本篇主要描述了公司內(nèi)部同事都必須遵守的一些開發(fā)規(guī)矩,如統(tǒng)一開...
    零一間閱讀 2,158評(píng)論 0 2

友情鏈接更多精彩內(nèi)容