開篇語
Map是一種常用的kv資料結構,程式設計中經常使用,且作為一種最基礎的資料結構,很多程式語言本身提供的api都會有實現,Go也不例外,今天我們將從一下三個方面為大家分析Go中的Map。
- 什麼是Map?
- Go中如何使用Map?
- 以及Go的Map實現機制是什麼樣?
希望通過這幾個方面的講解,讓大家真正理解Go的Map使用和實現。
什麼是Map
key,value儲存
最通俗的話說Map是一種通過key來獲取value的一個資料結構,其底層儲存方式為陣列,在儲存時key不能重複,當key重複時,value進行覆蓋,我們通過key進行hash運算(可以簡單理解為把key轉化為一個整形數字)然後對陣列的長度取餘,得到key儲存在陣列的哪個下標位置,最後將key和value組裝為一個結構體,放入陣列下標處,看下圖:
-
length = len(array) = 4 hashkey1 = hash(xiaoming) = 4 index1 = hashkey1% length= 0 hashkey2 = hash(xiaoli) = 6 index2 = hashkey2% length= 2
hash衝突
如上圖所示,陣列一個下標處只能儲存一個元素,也就是說一個陣列下標只能儲存一對key,value, hashkey(xiaoming)=4佔用了下標0的位置,假設我們遇到另一個key,hashkey(xiaowang)也是4,這就是hash衝突(不同的key經過hash之後得到的值一樣),那麼key=xiaowang的怎麼儲存?
hash衝突的常見解決方法
- 開放定址法
也就是說當我們儲存一個key,value時,發現hashkey(key)的下標已經被別key佔用,那我們在這個陣列中空間中重新找一個沒被佔用的儲存這個衝突的key,那麼沒被佔用的有很多,找哪個好呢?常見的有線性探測法,線性補償探測法,隨機探測法,這裡我們主要說一下線性探測法線性探測,字面意思就是按照順序來,從衝突的下標處開始往後探測,到達陣列末尾時,從陣列開始處探測,直到找到一個空位置儲存這個key,當陣列都找不到的情況下回擴容(事實上當陣列容量快滿的時候就會擴容了);查詢某一個key的時候,找到key對應的下標,比較key是否相等,如果相等直接取出來,否則按照順尋探測直到碰到一個空位置,說明key不存在。如下圖:
首先儲存key=xiaoming在下標0處,當儲存key=xiaowang時,hash衝突了,按照線性探測,儲存在下標1處,(紅色的線是衝突或者下標已經被佔用了)
再者key=xiaozhao儲存在下標4處,當儲存key=xiaoliu是,hash衝突了,按照線性探測,從頭開始,儲存在下標2處 (黃色的是衝突或者下標已經被佔用了)- 拉鍊法
何為拉鍊,簡單理解為連結串列,當key的hash衝突時,我們在衝突位置的元素上形成一個連結串列,通過指標互連線,當查詢時,發現key衝突,順著連結串列一直往下找,直到連結串列的尾節點,找不到則返回空,如下圖:
- 開放定址(線性探測)和拉鍊的優缺點
- 由上面可以看出拉鍊法比線性探測處理簡單
- 線性探測查詢時會被拉鍊法會更消耗時間
- 線性探測會更加容易導致擴容,而拉鍊不會
- 拉鍊儲存了指標,所以空間上會比線性探測佔用多一點
- 拉鍊是動態申請儲存空間的,所以更適合鏈長不確定的
Go中Map的使用
-
直接用程式碼描述,直觀,簡單,易理解
-
//直接建立初始化一個mao var mapInit = map[string]string {"xiaoli":"湖南", "xiaoliu":"天津"} //宣告一個map型別變數, //map的key的型別是string,value的型別是string var mapTemp map[string]string //使用make函式初始化這個變數,並指定大小(也可以不指定) mapTemp = make(map[string]string,10) //儲存key ,value mapTemp["xiaoming"] = "北京" mapTemp["xiaowang"]= "河北" //根據key獲取value, //如果key存在,則ok是true,否則是flase //v1用來接收key對應的value,當ok是false時,v1是nil v1,ok := mapTemp["xiaoming"] fmt.Println(ok,v1) //當key=xiaowang存在時列印value if v2,ok := mapTemp["xiaowang"]; ok{ fmt.Println(v2) } //遍歷map,列印key和value for k,v := range mapTemp{ fmt.Println(k,v) } //刪除map中的key delete(mapTemp,"xiaoming") //獲取map的大小 l := len(mapTemp) fmt.Println(l)
-
看了上面的map建立,初始化,增刪改查等操作,我們發現go的api其實挺簡單易學的
Go中Map的實現原理
知其然,更得知其所以然,會使用map了,多問問為什麼,go底層map到底怎麼儲存呢?接下來我們一探究竟。
map的原始碼位於 src/runtime/map.go中 筆者go的版本是1.12
在go中,map同樣也是陣列儲存的的,每個陣列下標處儲存的是一個bucket,這個bucket的型別見下面程式碼,每個bucket中可以儲存8個kv鍵值對,當每個bucket儲存的kv對到達8個之後,會通過overflow指標指向一個新的bucket,從而形成一個連結串列,看bmap的結構,我想大家應該很納悶,沒看見kv的結構和overflow指標啊,事實上,這兩個結構體並沒有顯示定義,是通過指標運算進行訪問的。
-
//bucket結構體定義 b就是bucket type bmap{ // tophash generally contains the top byte of the hash value for each key in this bucket. If tophash[0] < minTopHash, tophash[0] is a bucket evacuation state instead. //翻譯:top hash通常包含該bucket中每個鍵的hash值的高八位。如果tophash[0]小於mintophash,則tophash[0]為桶疏散狀態 //bucketCnt 的初始值是8 tophash [bucketCnt]uint8 // Followed by bucketCnt keys and then bucketCnt values. NOTE: packing all the keys together and then all the values together makes the code a bit more complicated than alternating key/value/key/value/... but it allows us to eliminate padding which would be needed for, e.g., map[int64]int8. Followed by an overflow pointer. //翻譯:接下來是bucketcnt鍵,然後是bucketcnt值。注意:將所有鍵打包在一起,然後將所有值打包在一起,使得//程式碼比交替鍵/值/鍵/值/更復雜。但它允許//我們消除可能需要的填充,例如map[int64]int8./後面跟一個溢位指標 }
看上面程式碼以及註釋,我們能得到bucket中儲存的kv是這樣的,tophash用來快速查詢key值是否在該bucket中,而不同每次都通過真值進行比較;還有kv的存放,為什麼不是k1v1,k2v2..... 而是k1k2...v1v2...,我們看上面的註釋說的 map[int64]int8,key是int64(8個位元組),value是int8(一個位元組),kv的長度不同,如果按照kv格式存放,則考慮記憶體對齊v也會佔用int64,而按照後者儲存時,8個v剛好佔用一個int64,從這個就可以看出go的map設計之巧妙。
最後我們分析一下go的整體記憶體結構,閱讀一下map儲存的原始碼,如下圖所示,當往map中儲存一個kv對時,通過k獲取hash值,hash值的低八位和bucket陣列長度取餘,定位到在陣列中的那個下標,hash值的高八位儲存在bucket中的tophash中,用來快速判斷key是否存在,key和value的具體值則通過指標運算儲存,當一個bucket滿時,通過overfolw指標連結到下一個bucket。
go的map儲存原始碼如下,省略了一些無關緊要的程式碼
-
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer { //獲取hash演算法 alg := t.key.alg //計算hash值 hash := alg.hash(key, uintptr(h.hash0)) //如果bucket陣列一開始為空,則初始化 if h.buckets == nil { // newarray(t.bucket, 1) h.buckets = newobject(t.bucket) } again: // 定位儲存在哪一個bucket中 bucket := hash & bucketMask(h.B) //得到bucket的結構體 b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) +bucket*uintptr(t.bucketsize))) //獲取高八位hash值 top := tophash(hash) var inserti *uint8 var insertk unsafe.Pointer var val unsafe.Pointer bucketloop: //死迴圈 for { //迴圈bucket中的tophash陣列 for i := uintptr(0); i < bucketCnt; i++ { //如果hash不相等 if b.tophash[i] != top { //判斷是否為空,為空則插入 if isEmpty(b.tophash[i]) && inserti == nil { inserti = &b.tophash[i] insertk = add(指標運算) val = add(指標運算) } //插入成功,終止最外層迴圈 if b.tophash[i] == emptyRest { break bucketloop } continue } //到這裡說明高八位hash一樣,獲取已存在的key k := add(指標運算) if t.indirectkey() { k = *((*unsafe.Pointer)(k)) } //判斷兩個key是否相等,不相等就迴圈下一個 if !alg.equal(key, k) { continue } // 如果相等則更新 if t.needkeyupdate() { typedmemmove(t.key, k, key) } //獲取已存在的value val = add(指標運算) goto done } //如果上一個bucket沒能插入,則通過overflow獲取連結串列上的下一個bucket ovf := b.overflow(t) if ovf == nil { break } b = ovf } if inserti == nil { // all current buckets are full, allocate a new one. newb := h.newoverflow(t, b) inserti = &newb.tophash[0] insertk = add(指標運算) } // store new key/value at insert position if t.indirectkey() { kmem := newobject(t.key) *(*unsafe.Pointer)(insertk) = kmem insertk = kmem } if t.indirectvalue() { vmem := newobject(t.elem) *(*unsafe.Pointer)(val) = vmem } typedmemmove(t.key, insertk, key) //將高八位hash值儲存 *inserti = top h.count++ return val }
歡迎大家關注微信公眾號:“golang那點事”,更多精彩期待你的到來