golangmap存儲函數
A. 徹底理解Golang Map
本文目錄如下,閱讀本文後,將一網打盡下面Golang Map相關面試題
Go中的map是一個指針,佔用8個位元組,指向hmap結構體; 源碼 src/runtime/map.go 中可以看到map的底層結構
每個map的底層結構是hmap,hmap包含若干個結構為bmap的bucket數組。每個bucket底層都採用鏈表結構。接下來,我們來詳細看下map的結構
bmap 就是我們常說的「桶」,一個桶裡面會最多裝 8 個 key,這些 key 之所以會落入同一個桶,是因為它們經過哈希計算後,哈希結果是「一類」的,關於key的定位我們在map的查詢和插入中詳細說明。在桶內,又會根據 key 計算出來的 hash 值的高 8 位來決定 key 到底落入桶內的哪個位置(一個桶內最多有8個位置)。
bucket內存數據結構可視化如下:
注意到 key 和 value 是各自放在一起的,並不是 key/value/key/value/... 這樣的形式。源碼里說明這樣的好處是在某些情況下可以省略掉 padding欄位,節省內存空間。
當 map 的 key 和 value 都不是指針,並且 size 都小於 128 位元組的情況下,會把 bmap 標記為不含指針,這樣可以避免 gc 時掃描整個 hmap。但是,我們看 bmap 其實有一個 overflow 的欄位,是指針類型的,破壞了 bmap 不含指針的設想,這時會把 overflow 移動到 extra 欄位來。
map是個指針,底層指向hmap,所以是個引用類型
golang 有三個常用的高級類型 slice 、map、channel, 它們都是 引用類型 ,當引用類型作為函數參數時,可能會修改原內容數據。
golang 中沒有引用傳遞,只有值和指針傳遞。所以 map 作為函數實參傳遞時本質上也是值傳遞,只不過因為 map 底層數據結構是通過指針指向實際的元素存儲空間,在被調函數中修改 map,對調用者同樣可見,所以 map 作為函數實參傳遞時表現出了引用傳遞的效果。
因此,傳遞 map 時,如果想修改map的內容而不是map本身,函數形參無需使用指針
map 底層數據結構是通過指針指向實際的元素 存儲空間 ,這種情況下,對其中一個map的更改,會影響到其他map
map 在沒有被修改的情況下,使用 range 多次遍歷 map 時輸出的 key 和 value 的順序可能不同。這是 Go 語言的設計者們有意為之,在每次 range 時的順序被隨機化,旨在提示開發者們,Go 底層實現並不保證 map 遍歷順序穩定,請大家不要依賴 range 遍歷結果順序。
map 本身是無序的,且遍歷時順序還會被隨機化,如果想順序遍歷 map,需要對 map key 先排序,再按照 key 的順序遍歷 map。
map默認是並發不安全的,原因如下:
Go 官方在經過了長時間的討論後,認為 Go map 更應適配典型使用場景(不需要從多個 goroutine 中進行安全訪問),而不是為了小部分情況(並發訪問),導致大部分程序付出加鎖代價(性能),決定了不支持。
場景: 2個協程同時讀和寫,以下程序會出現致命錯誤:fatal error: concurrent map writes
如果想實現map線程安全,有兩種方式:
方式一:使用讀寫鎖 map + sync.RWMutex
方式二:使用golang提供的 sync.Map
sync.map是用讀寫分離實現的,其思想是空間換時間。和map+RWLock的實現方式相比,它做了一些優化:可以無鎖訪問read map,而且會優先操作read map,倘若只操作read map就可以滿足要求(增刪改查遍歷),那就不用去操作write map(它的讀寫都要加鎖),所以在某些特定場景中它發生鎖競爭的頻率會遠遠小於map+RWLock的實現方式。
golang中map是一個kv對集合。底層使用hash table,用鏈表來解決沖突 ,出現沖突時,不是每一個key都申請一個結構通過鏈表串起來,而是以bmap為最小粒度掛載,一個bmap可以放8個kv。在哈希函數的選擇上,會在程序啟動時,檢測 cpu 是否支持 aes,如果支持,則使用 aes hash,否則使用 memhash。
map有3鍾初始化方式,一般通過make方式創建
map的創建通過生成匯編碼可以知道,make創建map時調用的底層函數是 runtime.makemap 。如果你的map初始容量小於等於8會發現走的是 runtime.fastrand 是因為容量小於8時不需要生成多個桶,一個桶的容量就可以滿足
makemap函數會通過 fastrand 創建一個隨機的哈希種子,然後根據傳入的 hint 計算出需要的最小需要的桶的數量,最後再使用 makeBucketArray 創建用於保存桶的數組,這個方法其實就是根據傳入的 B 計算出的需要創建的桶數量在內存中分配一片連續的空間用於存儲數據,在創建桶的過程中還會額外創建一些用於保存溢出數據的桶,數量是 2^(B-4) 個。初始化完成返回hmap指針。
找到一個 B,使得 map 的裝載因子在正常范圍內
Go 語言中讀取 map 有兩種語法:帶 comma 和 不帶 comma。當要查詢的 key 不在 map 里,帶 comma 的用法會返回一個 bool 型變數提示 key 是否在 map 中;而不帶 comma 的語句則會返回一個 value 類型的零值。如果 value 是 int 型就會返回 0,如果 value 是 string 類型,就會返回空字元串。
map的查找通過生成匯編碼可以知道,根據 key 的不同類型,編譯器會將查找函數用更具體的函數替換,以優化效率:
函數首先會檢查 map 的標志位 flags。如果 flags 的寫標志位此時被置 1 了,說明有其他協程在執行「寫」操作,進而導致程序 panic。這也說明了 map 對協程是不安全的。
key經過哈希函數計算後,得到的哈希值如下(主流64位機下共 64 個 bit 位):
m: 桶的個數
從buckets 通過 hash & m 得到對應的bucket,如果bucket正在擴容,並且沒有擴容完成,則從oldbuckets得到對應的bucket
計算hash所在桶編號:
用上一步哈希值最後的 5 個 bit 位,也就是 01010 ,值為 10,也就是 10 號桶(范圍是0~31號桶)
計算hash所在的槽位:
用上一步哈希值哈希值的高8個bit 位,也就是 10010111 ,轉化為十進制,也就是151,在 10 號 bucket 中尋找** tophash 值(HOB hash)為 151* 的 槽位**,即為key所在位置,找到了 2 號槽位,這樣整個查找過程就結束了。
如果在 bucket 中沒找到,並且 overflow 不為空,還要繼續去 overflow bucket 中尋找,直到找到或是所有的 key 槽位都找遍了,包括所有的 overflow bucket。
通過上面找到了對應的槽位,這里我們再詳細分析下key/value值是如何獲取的:
bucket 里 key 的起始地址就是 unsafe.Pointer(b)+dataOffset。第 i 個 key 的地址就要在此基礎上跨過 i 個 key 的大小;而我們又知道,value 的地址是在所有 key 之後,因此第 i 個 value 的地址還需要加上所有 key 的偏移。
通過匯編語言可以看到,向 map 中插入或者修改 key,最終調用的是 mapassign 函數。
實際上插入或修改 key 的語法是一樣的,只不過前者操作的 key 在 map 中不存在,而後者操作的 key 存在 map 中。
mapassign 有一個系列的函數,根據 key 類型的不同,編譯器會將其優化為相應的「快速函數」。
我們只用研究最一般的賦值函數 mapassign 。
map的賦值會附帶著map的擴容和遷移,map的擴容只是將底層數組擴大了一倍,並沒有進行數據的轉移,數據的轉移是在擴容後逐步進行的,在遷移的過程中每進行一次賦值(access或者delete)會至少做一次遷移工作。
1.判斷map是否為nil
每一次進行賦值/刪除操作時,只要oldbuckets != nil 則認為正在擴容,會做一次遷移工作,下面會詳細說下遷移過程
根據上面查找過程,查找key所在位置,如果找到則更新,沒找到則找空位插入即可
經過前面迭代尋找動作,若沒有找到可插入的位置,意味著需要擴容進行插入,下面會詳細說下擴容過程
通過匯編語言可以看到,向 map 中刪除 key,最終調用的是 mapdelete 函數
刪除的邏輯相對比較簡單,大多函數在賦值操作中已經用到過,核心還是找到 key 的具體位置。尋找過程都是類似的,在 bucket 中挨個 cell 尋找。找到對應位置後,對 key 或者 value 進行「清零」操作,將 count 值減 1,將對應位置的 tophash 值置成 Empty
再來說觸發 map 擴容的時機:在向 map 插入新 key 的時候,會進行條件檢測,符合下面這 2 個條件,就會觸發擴容:
1、裝載因子超過閾值
源碼里定義的閾值是 6.5 (loadFactorNum/loadFactorDen),是經過測試後取出的一個比較合理的因子
我們知道,每個 bucket 有 8 個空位,在沒有溢出,且所有的桶都裝滿了的情況下,裝載因子算出來的結果是 8。因此當裝載因子超過 6.5 時,表明很多 bucket 都快要裝滿了,查找效率和插入效率都變低了。在這個時候進行擴容是有必要的。
對於條件 1,元素太多,而 bucket 數量太少,很簡單:將 B 加 1,bucket 最大數量( 2^B )直接變成原來 bucket 數量的 2 倍。於是,就有新老 bucket 了。注意,這時候元素都在老 bucket 里,還沒遷移到新的 bucket 來。新 bucket 只是最大數量變為原來最大數量的 2 倍( 2^B * 2 ) 。
2、overflow 的 bucket 數量過多
在裝載因子比較小的情況下,這時候 map 的查找和插入效率也很低,而第 1 點識別不出來這種情況。表面現象就是計算裝載因子的分子比較小,即 map 里元素總數少,但是 bucket 數量多(真實分配的 bucket 數量多,包括大量的 overflow bucket)
不難想像造成這種情況的原因:不停地插入、刪除元素。先插入很多元素,導致創建了很多 bucket,但是裝載因子達不到第 1 點的臨界值,未觸發擴容來緩解這種情況。之後,刪除元素降低元素總數量,再插入很多元素,導致創建很多的 overflow bucket,但就是不會觸發第 1 點的規定,你能拿我怎麼辦?overflow bucket 數量太多,導致 key 會很分散,查找插入效率低得嚇人,因此出台第 2 點規定。這就像是一座空城,房子很多,但是住戶很少,都分散了,找起人來很困難
對於條件 2,其實元素沒那麼多,但是 overflow bucket 數特別多,說明很多 bucket 都沒裝滿。解決辦法就是開辟一個新 bucket 空間,將老 bucket 中的元素移動到新 bucket,使得同一個 bucket 中的 key 排列地更緊密。這樣,原來,在 overflow bucket 中的 key 可以移動到 bucket 中來。結果是節省空間,提高 bucket 利用率,map 的查找和插入效率自然就會提升。
由於 map 擴容需要將原有的 key/value 重新搬遷到新的內存地址,如果有大量的 key/value 需要搬遷,會非常影響性能。因此 Go map 的擴容採取了一種稱為「漸進式」的方式,原有的 key 並不會一次性搬遷完畢,每次最多隻會搬遷 2 個 bucket。
上面說的 hashGrow() 函數實際上並沒有真正地「搬遷」,它只是分配好了新的 buckets,並將老的 buckets 掛到了 oldbuckets 欄位上。真正搬遷 buckets 的動作在 growWork() 函數中,而調用 growWork() 函數的動作是在 mapassign 和 mapdelete 函數中。也就是插入或修改、刪除 key 的時候,都會嘗試進行搬遷 buckets 的工作。先檢查 oldbuckets 是否搬遷完畢,具體來說就是檢查 oldbuckets 是否為 nil。
如果未遷移完畢,賦值/刪除的時候,擴容完畢後(預分配內存),不會馬上就進行遷移。而是採取 增量擴容 的方式,當有訪問到具體 bukcet 時,才會逐漸的進行遷移(將 oldbucket 遷移到 bucket)
nevacuate 標識的是當前的進度,如果都搬遷完,應該和2^B的長度是一樣的
在evacuate 方法實現是把這個位置對應的bucket,以及其沖突鏈上的數據都轉移到新的buckets上。
轉移的判斷直接通過tophash 就可以,判斷tophash中第一個hash值即可
遍歷的過程,就是按順序遍歷 bucket,同時按順序遍歷 bucket 中的 key。
map遍歷是無序的,如果想實現有序遍歷,可以先對key進行排序
為什麼遍歷 map 是無序的?
如果發生過遷移,key 的位置發生了重大的變化,有些 key 飛上高枝,有些 key 則原地不動。這樣,遍歷 map 的結果就不可能按原來的順序了。
如果就一個寫死的 map,不會向 map 進行插入刪除的操作,按理說每次遍歷這樣的 map 都會返回一個固定順序的 key/value 序列吧。但是 Go 杜絕了這種做法,因為這樣會給新手程序員帶來誤解,以為這是一定會發生的事情,在某些情況下,可能會釀成大錯。
Go 做得更絕,當我們在遍歷 map 時,並不是固定地從 0 號 bucket 開始遍歷,每次都是從一個**隨機值序號的 bucket 開始遍歷,並且是從這個 bucket 的一個 隨機序號的 cell **開始遍歷。這樣,即使你是一個寫死的 map,僅僅只是遍歷它,也不太可能會返回一個固定序列的 key/value 對了。
B. golang map的元素遍歷為什麼是隨機的
Map是隨機存儲的,好像是按內存塊的大小放數據。這樣存儲效率高。但檢索效率低。List是會重新劃分存儲空間,保證連續存儲,存的效率低,檢索效率高。大概是這個意思,具體的,准確、詳細的自己google下。
hashCode() 方法得到其 hashCode 值——每個 java 對象都有 hashCode() 方法,都可通過該方法獲得它的 hashCode 值。得到這個對象的 hashCode 值之後,系統會根據該 hashCode 值來決定該元素的存儲位置。
設置了首尾倒置函數,也會出現這種類似情況。還有,你要注意:map中不允許存在重復的鍵名,你也可以使用其他的方式來實現,比如List,排序的話還得靠你自己來實現了。
C. golang變數(二)——map和slice詳解
衍生類型,interface{} , map, [] ,struct等
map類似於java的hashmap,python的dict,php的hash array。
常規的for循環,可以用for k,v :=range m {}. 但在下面清空有一個坑注意:
著名的map[string]*struct 副本問題
結果:
Go 中不存在引用傳遞,所有的參數傳遞都是值傳遞,而map是等同於指針類型的,所以在把map變數傳遞給函數時,函數對map的修改,也會實質改變map的值。
slice類似於其他語言的數組(list,array),slice初始化和map一樣,這里不在重復
除了Pointer數組外,len表示使用長度,cap是總容量,make([]int, len, cap)可以預申請 比較大的容量,這樣可以減少容量拓展的消耗,前提是要用到。
cap是計算切片容量,len是計算變數長度的,兩者不一樣。具體例子如下:
結果:
分析:cap是計算當前slice已分配的容量大小,採用的是預分配的夥伴演算法(當容量滿時,拓展分配一倍的容量)。
append是slice非常常用的函數,用於添加數據到slice中,但如果使用不好,會有下面的問題:
預期是[1 2 3 4 5 6 7 8 9 10], [1 2 3 4 5 6 7 8 9 10 11 12],但實際結果是:
注意slice是值傳遞,修改一下:
輸出如下:
== 只能用於判斷常規數據類型,無法使用用於slice和map判斷,用於判斷map和slice可以使用reflect.DeepEqual,這個函數用了遞歸來判斷每層的k,v是否一致。
當然還有其他方式,比如轉換成json,但小心有一些異常的bug,比如html編碼,具體這個json問題,待後面在分析。
D. Go語言——sync.Map詳解
sync.Map是1.9才推薦的並發安全的map,除了互斥量以外,還運用了原子操作,所以在這之前,有必要了解下 Go語言——原子操作
go1.10srcsyncmap.go
entry分為三種情況:
從read中讀取key,如果key存在就tryStore。
注意這里開始需要加鎖,因為需要操作dirty。
條目在read中,首先取消標記,然後將條目保存到dirty里。(因為標記的數據不在dirty里)
最後原子保存value到條目裡面,這里注意read和dirty都有條目。
總結一下Store:
這里可以看到dirty保存了數據的修改,除非可以直接原子更新read,繼續保持read clean。
有了之前的經驗,可以猜測下load流程:
與猜測的 區別 :
由於數據保存兩份,所以刪除考慮:
先看第二種情況。加鎖直接刪除dirty數據。思考下貌似沒什麼問題,本身就是臟數據。
第一種和第三種情況唯一的區別就是條目是否被標記。標記代表刪除,所以直接返回。否則CAS操作置為nil。這里總感覺少點什麼,因為條目其實還是存在的,雖然指針nil。
看了一圈貌似沒找到標記的邏輯,因為刪除只是將他變成nil。
之前以為這個邏輯就是簡單的將為標記的條目拷貝給dirty,現在看來大有文章。
p == nil,說明條目已經被delete了,CAS將他置為標記刪除。然後這個條目就不會保存在dirty裡面。
這里其實就跟miss邏輯串起來了,因為miss達到閾值之後,dirty會全量變成read,也就是說標記刪除在這一步最終刪除。這個還是很巧妙的。
真正的刪除邏輯:
很繞。。。。
E. golang map源碼淺析
golang 中 map的實現結構為: 哈希表 + 鏈表。 其中鏈表,作用是當發生hash沖突時,拉鏈法生成的結點。
可以看到, []bmap 是一個hash table, 每一個 bmap是我們常說的「桶」。 經過hash 函數計算出來相同的hash值, 放到相同的桶中。 一個 bmap中可以存放 8個 元素, 如果多出8個,則生成新的結點,尾接到隊尾。
以上是只是靜態文件 src/runtime/map.go 中的定義。 實際上編譯期間會給它加料 ,動態地創建一個新的結構:
上圖就是 bmap的內存模型, HOB Hash 指的就是 top hash。 注意到 key 和 value 是各自放在一起的,並不是 key/value/key/value/... 這樣的形式。源碼里說明這樣的好處是在某些情況下可以省略掉 padding 欄位,節省內存空間。
每個 bmap設計成 最多隻能放 8 個 key-value 對 ,如果有第 9 個 key-value 落入當前的 bmap,那就需要再構建一個 bmap,通過 overflow 指針連接起來。
map創建方法:
我們實際上是通過調用的 makemap ,來創建map的。實際工作只是初始化了hmap中的各種欄位,如:設置B的大小, 設置hash 種子 hash 0.
注意 :
makemap 返回是*hmap 指針, 即 map 是引用對象, 對map的操作會影響到結構體內部 。
使用方式
對應的是下面兩種方法
map的key的類型,實現了自己的hash 方式。每種類型實現hash函數方式不一樣。
key 經過哈希計算後得到hash值,共 64 個 bit 位。 其中後B 個bit位置, 用來定位當前元素落在哪一個桶里, 高8個bit 為當前 hash 值的top hash。 實際上定位key的過程是一個雙重循環的過程, 外層循環遍歷 所有的overflow, 內層循環遍歷 當前bmap 中的 8個元素 。
舉例說明: 如果當前 B 的值為 5, 那麼buckets 的長度 為 2^5 = 32。假設有個key 經過hash函數計算後,得到的hash結果為:
外層遍歷bucket 中的鏈表
內層循環遍歷 bmap中的8個 cell
建議先不看此部分內容,看完後續 修改 map中元素 -> 擴容 操作後 再回頭看此部分內容。
擴容前的數據:
等量擴容後的數據:
等量擴容後,查找方式和原本相同, 不多做贅述。
兩倍擴容後的數據
兩倍擴容後,oldbuckets 的元素,可能被分配成了兩部分。查找順序如下:
此處只分析 mapaccess1 ,。 mapaccess2 相比 mapaccess1 多添加了是否找到的bool值, 有興趣可自行看一下。
使用方式:
步驟如下:
擴容條件 :
擴容的標識 : h.oldbuckets != nil
假設當前定位到了新的buckets的3號桶中,首先會判斷oldbuckets中的對應的桶有沒有被搬遷過。 如果搬遷過了,不需要看原來的桶了,直接遍歷新的buckets的3號桶。
擴容前:
等量擴容結果
雙倍擴容會將old buckets上的元素分配到x, y兩個部key & 1 << B == 0 分配到x部分,key & 1 << B == 1 分配到y部分
注意: 當前只對雙倍擴容描述, 等量擴容只是重新填充了一下元素, 相對位置沒有改變。
假設當前map 的B == 5,原本元素經過hash函數計算的 hash 值為:
因為雙倍擴容之後 B = B + 1,此時B == 6。key & 1 << B == 1, 即 當前元素rehash到高位,新buckets中 y 部分. 否則 key & 1 << B == 0 則rehash到低位,即x 部分。
使用方式:
可以看到,每一遍歷生成迭代器的時候,會隨機選取一個bucket 以及 一個cell開始。 從前往後遍歷,再次遍歷到起始位置時,遍歷完成。
https://www.qcrao.com/2019/05/22/dive-into-go-map/
https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/
https://www.bilibili.com/video/BV1Q4411W7MR?spm_id_from=333.337.search-card.all.click
F. golang map array 是怎麼存儲的
map 的 hash 表包含了一個桶集合(collection of buckets)。當我們存儲,移除或者查找鍵值對(key/value pair)時,都會從選擇一個桶開始。在映射(map)操作過程中,我們會把指定的鍵值(key)傳遞給 hash 函數(又稱散列函數)。hash 函數的作用是生成索引,索引均勻的分布在所有可用的桶上。hash 表演算法詳見:July的博客—從頭到尾徹底解析 hash 表演算法
G. golang 實現bitmap
type bitmap struct {
keys []byte
len int
}
func NewBitMap() *bitmap {
return &bitmap{keys:make([]byte, 0), len:0}
}
func (b *bitmap)has(v int) bool {
k := v /8
kv :=byte(v %8)
if k >len(b.keys) { //todo not exist
return false
}
if b.keys[k]&(1<<kv) != 0 {
return true
}
return false
}
func (b *bitmap)set(v int) {
k := v /8
kv :=byte(v %8)
for b.len <= k {
b.keys =append(b.keys, 0)
b.len++
}
b.keys[k] =b.keys[k] | (1 << kv)
}
func (b *bitmap)length()int {
return b.len
}
func (b *bitmap)print() {
for _, v :=range b.keys {
fmt.Printf("%08b\n", v)
}
}
H. Go語言使用 map 時盡量不要在 big map 中保存指針
不知道你有沒有聽過這么一句:在使用 map 時盡量不要在 big map 中保存指針。好吧,你現在已經聽過了:)為什麼呢?原因在於 Go 語言的垃圾回收器會掃描標記 map 中的所有元素,GC 開銷相當大,直接GG。
這兩天在《Mastering Go》中看到 GC 這一章節裡面對比 map 和 slice 在垃圾回收中的效率對比,書中只給出結論沒有說明理由,這我是不能忍的,於是有了這篇學習筆記。扯那麼多,Show Your Code
這是一個簡單的測試程序,保存字元串的 map 和 保存整形的 map GC 的效率相差幾十倍,是不是有同學會說明明保存的是 string 哪有指針?這個要說到 Go 語言中 string 的底層實現了,源碼在 src/runtime/string.go里,可以看到 string 其實包含一個指向數據的指針和一個長度欄位。注意這里的是否包含指針,包括底層的實現。
Go 語言的 GC 會遞歸遍歷並標記所有可觸達的對象,標記完成之後將所有沒有引用的對象進行清理。掃描到指針就會往下接著尋找,一直到結束。
Go 語言中 map 是基於 數組和鏈表 的數據結構實現的,通過 優化的拉鏈法 解決哈希沖突,每個 bucket 可以保存 8 對鍵值,在 8 個鍵值對數據後面有一個 overflow 指針,因為桶中最多隻能裝 8 個鍵值對,如果有多餘的鍵值對落到了當前桶,那麼就需要再構建一個桶(稱為溢出桶),通過 overflow 指針鏈接起來。
因為 overflow 指針的緣故,所以無論 map 保存的是什麼,GC 的時候就會把所有的 bmap 掃描一遍,帶來巨大的 GC 開銷。官方 issues 就有關於這個問題的討論, runtime: Large maps cause significant GC pauses #9477
無腦機翻如下:
如果我們有一個map [k] v,其中k和v都不包含指針,並且我們想提高掃描性能,則可以執行以下操作。
將「 allOverflow [] unsafe.Pointer」添加到 hmap 並將所有溢出存儲桶存儲在其中。 然後將 bmap 標記為noScan。 這將使掃描非常快,因為我們不會掃描任何用戶數據。
實際上,它將有些復雜,因為我們需要從allOverflow中刪除舊的溢出桶。 而且它還會增加 hmap 的大小,因此也可能需要重新整理數據。
最終官方在 hmap 中增加了 overflow 相關欄位完成了上面的優化,這是具體的 commit 地址。
下面看下具體是如何實現的,源碼基於 go1.15,src/cmd/compile/internal/gc/reflect.go 中
通過注釋可以看出,如果 map 中保存的鍵值都不包含指針(通過 Haspointers 判斷),就使用一個 uintptr 類型代替 bucket 的指針用於溢出桶 overflow 欄位,uintptr 類型在 GO 語言中就是個大小可以保存得下指針的整數,不是指針,就相當於實現了 將 bmap 標記為 noScan, GC 的時候就不會遍歷完整個 map 了。隨著不斷的學習,愈發感慨 GO 語言中很多模塊設計得太精妙了。
差不多說清楚了,能力有限,有不對的地方歡迎留言討論,源碼位置還是問的群里大佬 _
I. golang 新人求助:關於map的定義和make函數
/* 聲明變數,默認 map 是 nil */
J. go語言怎樣處理 map 的值
// 先聲明map
var m1 map[string]string
// 再使用make函數創建一個非nil的map,nil map不能賦值
m1 = make(map[string]string)
// 最後給已聲明的map賦值
m1["a"] = "aa"
m1["b"] = "bb"
// 直接創建
m2 := make(map[string]string)
// 然後賦值
m2["a"] = "aa"
m2["b"] = "bb"
// 初始化 + 賦值一體化
m3 := map[string]string{
"a": "aa",
"b": "bb",
}
望採納。。
// ==========================================
// 查找鍵值是否存在
if v, ok := m1["a"]; ok {
fmt.Println(v)
} else {
fmt.Println("Key Not Found")
}
// 遍歷map
for k, v := range m1 {
fmt.Println(k, v)
}