網上分析golang中map的源碼的博客已經很是多了,隨便一搜就有,並且也很是詳細,因此若是我再來寫就有點多此一舉了(並且我也寫很差,手動滑稽)。可是我仍是要寫,略略略,這篇博客的意義在於能從幾張圖片,而後用我最通俗的文字,讓沒看過源碼的人最快程度上了解golang中map是怎麼樣的。html
固然,由於簡單,因此不完美。有不少地方省略了細節問題,若是你以爲沒看夠,或者原本就想了解詳細狀況的話在文末給出了一些很是不錯的博客,固然有能力仍是本身去閱讀源碼比較靠譜。java
那麼下面我將從這幾個方面來講明,你先記住有下面幾個方向,這樣能夠有一個大體的思路:git
這個就是golang中map的結構,其實真的不復雜,我省略了其中一些和結構關係不大的字段,就只剩下這些了。github
大話來描述一些要點:golang
這就是map的結構,而後咱們稍微對比總結一下。數組
咱們常見的map如java中的map是直接拿數組,數組中直接對應出了key-value,而在golang中,作了多加中間一層,buckets;java中若是key的哈希相同會採用鏈表的方式鏈接下去,當達到必定程度會轉換紅黑樹,golang中直接相似鏈表鏈接下去,只不過鏈接下去的是buckets。緩存
那麼看完結構你確定會有疑問?爲何要多一層8個格子的bucket呢?咱們怎麼肯定放在8個格子其中的哪一個呢?帶着問題往下看。
安全
初始化就不須要圖去說明了,由於初始化以後就是產生基礎的一個結構,根據map中存放的類型不一樣。這裏主要說明一下,初始化的代碼放在什麼位置。我也刪除了其中一些代碼,大體看看就好。數據結構
// makehmap_small implements Go map creation for make(map[k]v) and // make(map[k]v, hint) when hint is known to be at most bucketCnt // at compile time and the map needs to be allocated on the heap. func makemap_small() *hmap { h := new(hmap) h.hash0 = fastrand() return h } // makemap implements Go map creation for make(map[k]v, hint). // If the compiler has determined that the map or the first bucket // can be created on the stack, h and/or bucket may be non-nil. // If h != nil, the map can be created directly in h. // If h.buckets != nil, bucket pointed to can be used as the first bucket. func makemap(t *maptype, hint int, h *hmap) *hmap { ..... // initialize Hmap if h == nil { h = (*hmap)(newobject(t.hmap)) } h.hash0 = fastrand() // find size parameter which will hold the requested # of elements B := uint8(0) for overLoadFactor(hint, B) { B++ } h.B = B ...... return h }
其中須要注意一個點:「B」,還記得剛纔說名字叫bmap的桶數量是不肯定的嗎?這個B必定程度上表示的就是桶的數量,固然不是說B是3桶的數量就是3,而是2的3次方,也就是8;當B爲5,桶的數量就是32;記住這個B,後面會用到它。併發
其實你想嘛,初始化還能幹什麼,最重要的確定就是肯定一開始要有多少個桶,初始的大小仍是很重要的,還有一些別的初始化哈希種子等等,問題不大。咱們的重點仍是要放在存/取上面。
其實從結構上面來看,咱們已經能夠摸到一些門道了。先本身想一下,要從一個hashmap中獲取一個元素,那麼必定是經過key的哈希值去定位到這個元素,那麼想着這個大體方向,看下面一張流程圖來詳細理解golang中是如何實現的。
下面說明要點:
總結一下:經過後B位肯定桶,經過前8位肯定格子,循環遍歷連着的全部桶所有找完爲止。
那麼爲何要有這個tophash呢?由於tophash能夠快速肯定key是否正確,你能夠把它理解成一種緩存措施,若是前8位都不對了,後面就沒有必要比較了。
其中紅色的字標出的地方說明了上面的關鍵點,最後有關key和value具體的存放方式和取出的定位不作深究,有興趣能夠看最後的參考博客。
其實當你知道了如何GET,那麼PUT就沒有什麼難度了,由於本質是同樣的。PUT的時候同樣的方式去定位key的位置:
這裏主要圖解說明一下,若是新來的key發現前面有一個格子空着(這個狀況是刪除形成的),就會記錄這個位置,當所有掃描完成以後發現本身確實是新來的,那麼就會放前面那個空着的,而不會放最後(我把這個稱爲緊湊原則,儘量保證數據存放緊湊,這樣下次掃描會快)
go/src/runtime/hashmap.go的mapassign函數就是map的put方法,由於代碼很長這裏就很少贅述了。
這個就是最複雜的地方了,可是呢?Don't worry我這裏仍是會省略其中某些部分,將最重要的地方拎出來。
若是你看過Java的HashMap實現,就知道有個裝載因子,一樣的在golang中也有,可是不同哦。裝載因子的定義是這個樣子:
loadFactor := count / (2B)
其中count爲map中元素的個數,B就是以前個那個「B」
翻譯一下就是裝載因子 = (map中元素的個數)/(map當前桶的個數)
裝載因子 > 6.5(這個值是源碼中寫的)
其實意思就是,桶只有那麼幾個,可是元素不少,證實有不少溢出桶的存在(能夠想成鏈表拉的太長了),那麼掃描速度會很慢,就要擴容。
overflow 的 bucket 數量過多:當 B 小於 15,若是 overflow 的 bucket 數量超過 2B ;當 B >= 15,若是 overflow 的 bucket 數量超過 215 。
其實意思就是,可能有一個單獨的一條鏈拉的很長,溢出桶太多了,說白了就是,加入的key不巧,後B位都同樣,一直落在同一個桶裏面,這個桶一直放,雖然裝載因子不高,可是掃描速度就很慢。
當前不能正在擴容
這張圖表示的就是相同容量的擴容,實際上就是一種整理,將分散的數據集合到一塊兒,提升掃描效率。(上面表示擴容以前,下面表示擴容以後)
這張圖表示的是就是2倍的擴容(上面表示擴容以前,下面表示擴容以後),若是有兩個key後三位分別是001和101,當B=2時,只有4個桶,只看最後兩位,這兩個key後兩位都是01因此在一個桶裏面;擴容以後B=3,就會有8個桶,看後面三位,因而它們就分到了不一樣的桶裏面。
下面說一些擴容時的細節:
擴容的三個條件,看到了嗎?這個地方在mapassign方法中。
這裏能夠看到,註釋也寫的很清楚,若是是加載因子超出了,那麼就2倍擴容,若是不是那麼就是由於太多溢出桶了,sameSizeGrow表示就是相同容量擴容
evacuate是搬運方法,這邊能夠看到,每次搬運是1到2個
evacuate實在是太長了,也很是複雜,可是狀況就是圖上描述的那樣,有興趣的能夠詳細去看,這裏不截圖說明了。
至此你應該對於golang中的map有一個基本的認識了,你還能夠去看看刪除,你還能夠去看看遍歷等等,相信有了上面的基本認識那麼應該不會難到你。下面有幾個小問題:
寫的倉促,不免疏漏,有問題的地方還請批評指正。
若是你但願看到源碼的各類細節講解,下面這幾篇是我學習的時候看的,供你參考,但願對你有幫助
https://github.com/qcrao/Go-Questions/tree/master/map
https://github.com/cch123/golang-notes/blob/master/map.md
https://draveness.me/golang-hashmap
https://lukechampine.com/hackmap.html
做者:LinkinStar
未經容許,不得轉載