go語言map使用 golang map key struct

徹底理解Golang Map

本文目錄如下,閱讀本文后,將一網(wǎng)打盡下面Golang Map相關面試題

10多年的合浦網(wǎng)站建設經(jīng)驗,針對設計、前端、開發(fā)、售后、文案、推廣等六對一服務,響應快,48小時及時工作處理。成都全網(wǎng)營銷推廣的優(yōu)勢是能夠根據(jù)用戶設備顯示端的尺寸不同,自動調(diào)整合浦建站的顯示方式,使網(wǎng)站能夠適用不同顯示終端,在瀏覽器中調(diào)整網(wǎng)站的寬度,無論在任何一種瀏覽器上瀏覽網(wǎng)站,都能展現(xiàn)優(yōu)雅布局與設計,從而大程度地提升瀏覽體驗。創(chuàng)新互聯(lián)建站從事“合浦網(wǎng)站設計”,“合浦網(wǎng)站推廣”以來,每個客戶項目都認真落實執(zhí)行。

Go中的map是一個指針,占用8個字節(jié),指向hmap結(jié)構(gòu)體; 源碼 src/runtime/map.go 中可以看到map的底層結(jié)構(gòu)

每個map的底層結(jié)構(gòu)是hmap,hmap包含若干個結(jié)構(gòu)為bmap的bucket數(shù)組。每個bucket底層都采用鏈表結(jié)構(gòu)。接下來,我們來詳細看下map的結(jié)構(gòu)

bmap 就是我們常說的“桶”,一個桶里面會最多裝 8 個 key,這些 key 之所以會落入同一個桶,是因為它們經(jīng)過哈希計算后,哈希結(jié)果是“一類”的,關于key的定位我們在map的查詢和插入中詳細說明。在桶內(nèi),又會根據(jù) key 計算出來的 hash 值的高 8 位來決定 key 到底落入桶內(nèi)的哪個位置(一個桶內(nèi)最多有8個位置)。

bucket內(nèi)存數(shù)據(jù)結(jié)構(gòu)可視化如下:

注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 這樣的形式。源碼里說明這樣的好處是在某些情況下可以省略掉 padding字段,節(jié)省內(nèi)存空間。

當 map 的 key 和 value 都不是指針,并且 size 都小于 128 字節(jié)的情況下,會把 bmap 標記為不含指針,這樣可以避免 gc 時掃描整個 hmap。但是,我們看 bmap 其實有一個 overflow 的字段,是指針類型的,破壞了 bmap 不含指針的設想,這時會把 overflow 移動到 extra 字段來。

map是個指針,底層指向hmap,所以是個引用類型

golang 有三個常用的高級類型 slice 、map、channel, 它們都是 引用類型 ,當引用類型作為函數(shù)參數(shù)時,可能會修改原內(nèi)容數(shù)據(jù)。

golang 中沒有引用傳遞,只有值和指針傳遞。所以 map 作為函數(shù)實參傳遞時本質(zhì)上也是值傳遞,只不過因為 map 底層數(shù)據(jù)結(jié)構(gòu)是通過指針指向?qū)嶋H的元素存儲空間,在被調(diào)函數(shù)中修改 map,對調(diào)用者同樣可見,所以 map 作為函數(shù)實參傳遞時表現(xiàn)出了引用傳遞的效果。

因此,傳遞 map 時,如果想修改map的內(nèi)容而不是map本身,函數(shù)形參無需使用指針

map 底層數(shù)據(jù)結(jié)構(gòu)是通過指針指向?qū)嶋H的元素 存儲空間 ,這種情況下,對其中一個map的更改,會影響到其他map

map 在沒有被修改的情況下,使用 range 多次遍歷 map 時輸出的 key 和 value 的順序可能不同。這是 Go 語言的設計者們有意為之,在每次 range 時的順序被隨機化,旨在提示開發(fā)者們,Go 底層實現(xiàn)并不保證 map 遍歷順序穩(wěn)定,請大家不要依賴 range 遍歷結(jié)果順序。

map 本身是無序的,且遍歷時順序還會被隨機化,如果想順序遍歷 map,需要對 map key 先排序,再按照 key 的順序遍歷 map。

map默認是并發(fā)不安全的,原因如下:

Go 官方在經(jīng)過了長時間的討論后,認為 Go map 更應適配典型使用場景(不需要從多個 goroutine 中進行安全訪問),而不是為了小部分情況(并發(fā)訪問),導致大部分程序付出加鎖代價(性能),決定了不支持。

場景: 2個協(xié)程同時讀和寫,以下程序會出現(xiàn)致命錯誤:fatal error: concurrent map writes

如果想實現(xiàn)map線程安全,有兩種方式:

方式一:使用讀寫鎖 map + sync.RWMutex

方式二:使用golang提供的 sync.Map

sync.map是用讀寫分離實現(xiàn)的,其思想是空間換時間。和map+RWLock的實現(xiàn)方式相比,它做了一些優(yōu)化:可以無鎖訪問read map,而且會優(yōu)先操作read map,倘若只操作read map就可以滿足要求(增刪改查遍歷),那就不用去操作write map(它的讀寫都要加鎖),所以在某些特定場景中它發(fā)生鎖競爭的頻率會遠遠小于map+RWLock的實現(xiàn)方式。

golang中map是一個kv對集合。底層使用hash table,用鏈表來解決沖突 ,出現(xiàn)沖突時,不是每一個key都申請一個結(jié)構(gòu)通過鏈表串起來,而是以bmap為最小粒度掛載,一個bmap可以放8個kv。在哈希函數(shù)的選擇上,會在程序啟動時,檢測 cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。

map有3鐘初始化方式,一般通過make方式創(chuàng)建

map的創(chuàng)建通過生成匯編碼可以知道,make創(chuàng)建map時調(diào)用的底層函數(shù)是 runtime.makemap 。如果你的map初始容量小于等于8會發(fā)現(xiàn)走的是 runtime.fastrand 是因為容量小于8時不需要生成多個桶,一個桶的容量就可以滿足

makemap函數(shù)會通過 fastrand 創(chuàng)建一個隨機的哈希種子,然后根據(jù)傳入的 hint 計算出需要的最小需要的桶的數(shù)量,最后再使用 makeBucketArray 創(chuàng)建用于保存桶的數(shù)組,這個方法其實就是根據(jù)傳入的 B 計算出的需要創(chuàng)建的桶數(shù)量在內(nèi)存中分配一片連續(xù)的空間用于存儲數(shù)據(jù),在創(chuàng)建桶的過程中還會額外創(chuàng)建一些用于保存溢出數(shù)據(jù)的桶,數(shù)量是 2^(B-4) 個。初始化完成返回hmap指針。

找到一個 B,使得 map 的裝載因子在正常范圍內(nèi)

Go 語言中讀取 map 有兩種語法:帶 comma 和 不帶 comma。當要查詢的 key 不在 map 里,帶 comma 的用法會返回一個 bool 型變量提示 key 是否在 map 中;而不帶 comma 的語句則會返回一個 value 類型的零值。如果 value 是 int 型就會返回 0,如果 value 是 string 類型,就會返回空字符串。

map的查找通過生成匯編碼可以知道,根據(jù) key 的不同類型,編譯器會將查找函數(shù)用更具體的函數(shù)替換,以優(yōu)化效率:

函數(shù)首先會檢查 map 的標志位 flags。如果 flags 的寫標志位此時被置 1 了,說明有其他協(xié)程在執(zhí)行“寫”操作,進而導致程序 panic。這也說明了 map 對協(xié)程是不安全的。

key經(jīng)過哈希函數(shù)計算后,得到的哈希值如下(主流64位機下共 64 個 bit 位):

m: 桶的個數(shù)

從buckets 通過 hash m 得到對應的bucket,如果bucket正在擴容,并且沒有擴容完成,則從oldbuckets得到對應的bucket

計算hash所在桶編號:

用上一步哈希值最后的 5 個 bit 位,也就是 01010 ,值為 10,也就是 10 號桶(范圍是0~31號桶)

計算hash所在的槽位:

用上一步哈希值哈希值的高8個bit 位,也就是 10010111 ,轉(zhuǎn)化為十進制,也就是151,在 10 號 bucket 中尋找** tophash 值(HOB hash)為 151* 的 槽位**,即為key所在位置,找到了 2 號槽位,這樣整個查找過程就結(jié)束了。

如果在 bucket 中沒找到,并且 overflow 不為空,還要繼續(xù)去 overflow bucket 中尋找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。

通過上面找到了對應的槽位,這里我們再詳細分析下key/value值是如何獲取的:

bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 個 key 的地址就要在此基礎上跨過 i 個 key 的大??;而我們又知道,value 的地址是在所有 key 之后,因此第 i 個 value 的地址還需要加上所有 key 的偏移。

通過匯編語言可以看到,向 map 中插入或者修改 key,最終調(diào)用的是 mapassign 函數(shù)。

實際上插入或修改 key 的語法是一樣的,只不過前者操作的 key 在 map 中不存在,而后者操作的 key 存在 map 中。

mapassign 有一個系列的函數(shù),根據(jù) key 類型的不同,編譯器會將其優(yōu)化為相應的“快速函數(shù)”。

我們只用研究最一般的賦值函數(shù) mapassign 。

map的賦值會附帶著map的擴容和遷移,map的擴容只是將底層數(shù)組擴大了一倍,并沒有進行數(shù)據(jù)的轉(zhuǎn)移,數(shù)據(jù)的轉(zhuǎn)移是在擴容后逐步進行的,在遷移的過程中每進行一次賦值(access或者delete)會至少做一次遷移工作。

1.判斷map是否為nil

每一次進行賦值/刪除操作時,只要oldbuckets != nil 則認為正在擴容,會做一次遷移工作,下面會詳細說下遷移過程

根據(jù)上面查找過程,查找key所在位置,如果找到則更新,沒找到則找空位插入即可

經(jīng)過前面迭代尋找動作,若沒有找到可插入的位置,意味著需要擴容進行插入,下面會詳細說下擴容過程

通過匯編語言可以看到,向 map 中刪除 key,最終調(diào)用的是 mapdelete 函數(shù)

刪除的邏輯相對比較簡單,大多函數(shù)在賦值操作中已經(jīng)用到過,核心還是找到 key 的具體位置。尋找過程都是類似的,在 bucket 中挨個 cell 尋找。找到對應位置后,對 key 或者 value 進行“清零”操作,將 count 值減 1,將對應位置的 tophash 值置成 Empty

再來說觸發(fā) map 擴容的時機:在向 map 插入新 key 的時候,會進行條件檢測,符合下面這 2 個條件,就會觸發(fā)擴容:

1、裝載因子超過閾值

源碼里定義的閾值是 6.5 (loadFactorNum/loadFactorDen),是經(jīng)過測試后取出的一個比較合理的因子

我們知道,每個 bucket 有 8 個空位,在沒有溢出,且所有的桶都裝滿了的情況下,裝載因子算出來的結(jié)果是 8。因此當裝載因子超過 6.5 時,表明很多 bucket 都快要裝滿了,查找效率和插入效率都變低了。在這個時候進行擴容是有必要的。

對于條件 1,元素太多,而 bucket 數(shù)量太少,很簡單:將 B 加 1,bucket 最大數(shù)量( 2^B )直接變成原來 bucket 數(shù)量的 2 倍。于是,就有新老 bucket 了。注意,這時候元素都在老 bucket 里,還沒遷移到新的 bucket 來。新 bucket 只是最大數(shù)量變?yōu)樵瓉碜畲髷?shù)量的 2 倍( 2^B * 2 ) 。

2、overflow 的 bucket 數(shù)量過多

在裝載因子比較小的情況下,這時候 map 的查找和插入效率也很低,而第 1 點識別不出來這種情況。表面現(xiàn)象就是計算裝載因子的分子比較小,即 map 里元素總數(shù)少,但是 bucket 數(shù)量多(真實分配的 bucket 數(shù)量多,包括大量的 overflow bucket)

不難想像造成這種情況的原因:不停地插入、刪除元素。先插入很多元素,導致創(chuàng)建了很多 bucket,但是裝載因子達不到第 1 點的臨界值,未觸發(fā)擴容來緩解這種情況。之后,刪除元素降低元素總數(shù)量,再插入很多元素,導致創(chuàng)建很多的 overflow bucket,但就是不會觸發(fā)第 1 點的規(guī)定,你能拿我怎么辦?overflow bucket 數(shù)量太多,導致 key 會很分散,查找插入效率低得嚇人,因此出臺第 2 點規(guī)定。這就像是一座空城,房子很多,但是住戶很少,都分散了,找起人來很困難

對于條件 2,其實元素沒那么多,但是 overflow bucket 數(shù)特別多,說明很多 bucket 都沒裝滿。解決辦法就是開辟一個新 bucket 空間,將老 bucket 中的元素移動到新 bucket,使得同一個 bucket 中的 key 排列地更緊密。這樣,原來,在 overflow bucket 中的 key 可以移動到 bucket 中來。結(jié)果是節(jié)省空間,提高 bucket 利用率,map 的查找和插入效率自然就會提升。

由于 map 擴容需要將原有的 key/value 重新搬遷到新的內(nèi)存地址,如果有大量的 key/value 需要搬遷,會非常影響性能。因此 Go map 的擴容采取了一種稱為“漸進式”的方式,原有的 key 并不會一次性搬遷完畢,每次最多只會搬遷 2 個 bucket。

上面說的 hashGrow() 函數(shù)實際上并沒有真正地“搬遷”,它只是分配好了新的 buckets,并將老的 buckets 掛到了 oldbuckets 字段上。真正搬遷 buckets 的動作在 growWork() 函數(shù)中,而調(diào)用 growWork() 函數(shù)的動作是在 mapassign 和 mapdelete 函數(shù)中。也就是插入或修改、刪除 key 的時候,都會嘗試進行搬遷 buckets 的工作。先檢查 oldbuckets 是否搬遷完畢,具體來說就是檢查 oldbuckets 是否為 nil。

如果未遷移完畢,賦值/刪除的時候,擴容完畢后(預分配內(nèi)存),不會馬上就進行遷移。而是采取 增量擴容 的方式,當有訪問到具體 bukcet 時,才會逐漸的進行遷移(將 oldbucket 遷移到 bucket)

nevacuate 標識的是當前的進度,如果都搬遷完,應該和2^B的長度是一樣的

在evacuate 方法實現(xiàn)是把這個位置對應的bucket,以及其沖突鏈上的數(shù)據(jù)都轉(zhuǎn)移到新的buckets上。

轉(zhuǎn)移的判斷直接通過tophash 就可以,判斷tophash中第一個hash值即可

遍歷的過程,就是按順序遍歷 bucket,同時按順序遍歷 bucket 中的 key。

map遍歷是無序的,如果想實現(xiàn)有序遍歷,可以先對key進行排序

為什么遍歷 map 是無序的?

如果發(fā)生過遷移,key 的位置發(fā)生了重大的變化,有些 key 飛上高枝,有些 key 則原地不動。這樣,遍歷 map 的結(jié)果就不可能按原來的順序了。

如果就一個寫死的 map,不會向 map 進行插入刪除的操作,按理說每次遍歷這樣的 map 都會返回一個固定順序的 key/value 序列吧。但是 Go 杜絕了這種做法,因為這樣會給新手程序員帶來誤解,以為這是一定會發(fā)生的事情,在某些情況下,可能會釀成大錯。

Go 做得更絕,當我們在遍歷 map 時,并不是固定地從 0 號 bucket 開始遍歷,每次都是從一個**隨機值序號的 bucket 開始遍歷,并且是從這個 bucket 的一個 隨機序號的 cell **開始遍歷。這樣,即使你是一個寫死的 map,僅僅只是遍歷它,也不太可能會返回一個固定序列的 key/value 對了。

Go語言——sync.Map詳解

sync.Map是1.9才推薦的并發(fā)安全的map,除了互斥量以外,還運用了原子操作,所以在這之前,有必要了解下 Go語言——原子操作

go1.10\src\sync\map.go

entry分為三種情況:

從read中讀取key,如果key存在就tryStore。

注意這里開始需要加鎖,因為需要操作dirty。

條目在read中,首先取消標記,然后將條目保存到dirty里。(因為標記的數(shù)據(jù)不在dirty里)

最后原子保存value到條目里面,這里注意read和dirty都有條目。

總結(jié)一下Store:

這里可以看到dirty保存了數(shù)據(jù)的修改,除非可以直接原子更新read,繼續(xù)保持read clean。

有了之前的經(jīng)驗,可以猜測下load流程:

與猜測的 區(qū)別 :

由于數(shù)據(jù)保存兩份,所以刪除考慮:

先看第二種情況。加鎖直接刪除dirty數(shù)據(jù)。思考下貌似沒什么問題,本身就是臟數(shù)據(jù)。

第一種和第三種情況唯一的區(qū)別就是條目是否被標記。標記代表刪除,所以直接返回。否則CAS操作置為nil。這里總感覺少點什么,因為條目其實還是存在的,雖然指針nil。

看了一圈貌似沒找到標記的邏輯,因為刪除只是將他變成nil。

之前以為這個邏輯就是簡單的將為標記的條目拷貝給dirty,現(xiàn)在看來大有文章。

p == nil,說明條目已經(jīng)被delete了,CAS將他置為標記刪除。然后這個條目就不會保存在dirty里面。

這里其實就跟miss邏輯串起來了,因為miss達到閾值之后,dirty會全量變成read,也就是說標記刪除在這一步最終刪除。這個還是很巧妙的。

真正的刪除邏輯:

很繞。。。。

Go語言使用 map 時盡量不要在 big map 中保存指針

不知道你有沒有聽過這么一句:在使用 map 時盡量不要在 big map 中保存指針。好吧,你現(xiàn)在已經(jīng)聽過了:)為什么呢?原因在于 Go 語言的垃圾回收器會掃描標記 map 中的所有元素,GC 開銷相當大,直接GG。

這兩天在《Mastering Go》中看到 GC 這一章節(jié)里面對比 map 和 slice 在垃圾回收中的效率對比,書中只給出結(jié)論沒有說明理由,這我是不能忍的,于是有了這篇學習筆記。扯那么多,Show Your Code

這是一個簡單的測試程序,保存字符串的 map 和 保存整形的 map GC 的效率相差幾十倍,是不是有同學會說明明保存的是 string 哪有指針?這個要說到 Go 語言中 string 的底層實現(xiàn)了,源碼在 src/runtime/string.go里,可以看到 string 其實包含一個指向數(shù)據(jù)的指針和一個長度字段。注意這里的是否包含指針,包括底層的實現(xiàn)。

Go 語言的 GC 會遞歸遍歷并標記所有可觸達的對象,標記完成之后將所有沒有引用的對象進行清理。掃描到指針就會往下接著尋找,一直到結(jié)束。

Go 語言中 map 是基于 數(shù)組和鏈表 的數(shù)據(jù)結(jié)構(gòu)實現(xiàn)的,通過 優(yōu)化的拉鏈法 解決哈希沖突,每個 bucket 可以保存 8 對鍵值,在 8 個鍵值對數(shù)據(jù)后面有一個 overflow 指針,因為桶中最多只能裝 8 個鍵值對,如果有多余的鍵值對落到了當前桶,那么就需要再構(gòu)建一個桶(稱為溢出桶),通過 overflow 指針鏈接起來。

因為 overflow 指針的緣故,所以無論 map 保存的是什么,GC 的時候就會把所有的 bmap 掃描一遍,帶來巨大的 GC 開銷。官方 issues 就有關于這個問題的討論, runtime: Large maps cause significant GC pauses #9477

無腦機翻如下:

如果我們有一個map [k] v,其中k和v都不包含指針,并且我們想提高掃描性能,則可以執(zhí)行以下操作。

將“ allOverflow [] unsafe.Pointer”添加到 hmap 并將所有溢出存儲桶存儲在其中。 然后將 bmap 標記為noScan。 這將使掃描非常快,因為我們不會掃描任何用戶數(shù)據(jù)。

實際上,它將有些復雜,因為我們需要從allOverflow中刪除舊的溢出桶。 而且它還會增加 hmap 的大小,因此也可能需要重新整理數(shù)據(jù)。

最終官方在 hmap 中增加了 overflow 相關字段完成了上面的優(yōu)化,這是具體的 commit 地址。

下面看下具體是如何實現(xiàn)的,源碼基于 go1.15,src/cmd/compile/internal/gc/reflect.go 中

通過注釋可以看出,如果 map 中保存的鍵值都不包含指針(通過 Haspointers 判斷),就使用一個 uintptr 類型代替 bucket 的指針用于溢出桶 overflow 字段,uintptr 類型在 GO 語言中就是個大小可以保存得下指針的整數(shù),不是指針,就相當于實現(xiàn)了 將 bmap 標記為 noScan, GC 的時候就不會遍歷完整個 map 了。隨著不斷的學習,愈發(fā)感慨 GO 語言中很多模塊設計得太精妙了。

差不多說清楚了,能力有限,有不對的地方歡迎留言討論,源碼位置還是問的群里大佬 _

golang hashmap的使用及實現(xiàn)

由于go語言是一個強類型的語言,因此hashmap也是有類型的,具體體現(xiàn)在key和value都必須指定類型,比如聲明一個key為string,value也是string的map,

需要這樣做

大部分類型都能做key,某些類型是不能的,共同的特點是: 不能使用== 來比較,包括: slice, map, function

在迭代的過程中是可以對map進行刪除和更新操作的,規(guī)則如下:

golang的map是hash結(jié)構(gòu)的,意味著平均訪問時間是O(1)的。同傳統(tǒng)的hashmap一樣,由一個個bucket組成:

那我們怎么訪問到對應的bucket呢,我們需要得到對應key的hash值

各個參數(shù)的意思:

目前采用的是這一行:

| 6.50 | 20.90 | 10.79 | 4.25 | 6.50 |

本文標題:go語言map使用 golang map key struct
文章路徑:http://bm7419.com/article2/dohhcoc.html

成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供網(wǎng)頁設計公司、用戶體驗、軟件開發(fā)動態(tài)網(wǎng)站、網(wǎng)站收錄、做網(wǎng)站

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

成都定制網(wǎng)站建設