Golang Map簡介以及底層原理
Map 簡介
在Go語言中提供了map數(shù)據(jù)結(jié)構(gòu)來存儲鍵值對數(shù)據(jù)。map的數(shù)據(jù)類型為map[K]V
,其中K為鍵的類型,V為值的類型。map的鍵類型必須支持==
操作符,用來比較兩個鍵是否相等。Go語言提供了4種內(nèi)置的map操作: len
、delete
、comparison
、assign
。
Map 定義
map_var := make(map[K]V) // 用make函數(shù)創(chuàng)建一個空的map,其中K和V分別為鍵和值的類型 map_var[key] = value // 向map中添加一個鍵值對 value := map_var[key] // 獲取指定鍵的值 delete(map_var, key) // 從map中刪除指定的鍵及其對應(yīng)的值
Map Iteration
Go語言提供了兩個方法來遍歷map中的所有鍵值對,分別是range
方法和Len()
方法。
// 使用range循環(huán)遍歷map中的所有鍵值對 for key, value := range map_var { // TODO ... } // 計算map中的元素數(shù)量 if len(map_var) > 0 { // TODO ... }
Map 的線程安全
在Go語言中,map是非線程安全的,在多線程并發(fā)訪問時可能導(dǎo)致程序報錯。當(dāng)map被多個協(xié)程同時訪問時,我們需要使用sync包中的sync.Mutex
來確保操作的原子性和并發(fā)安全。
import "sync" type SafeMap struct { mu sync.Mutex m map[string]int } func (sm *SafeMap) Get(key string) int { sm.mu.Lock() defer sm.mu.Unlock() return sm.m[key] } func (sm *SafeMap) Set(key string, value int) { sm.mu.Lock() defer sm.mu.Unlock() sm.m[key] = value } func (sm *SafeMap) Delete(key string) { sm.mu.Lock() defer sm.mu.Unlock() delete(sm.m, key) }
map 底層原理
Go語言的map在設(shè)計上是一種哈希表的數(shù)據(jù)結(jié)構(gòu)。它利用哈希函數(shù)將鍵映射到不同的存儲空間,從而實(shí)現(xiàn)高效的查找和插入操作。
哈希函數(shù)
哈希函數(shù)將字符串映射到一個整數(shù)上,這稱為哈希值。不同的字符串可能會有相同的哈希值,但相同的字符串必定具有相同的哈希值。哈希函數(shù)需要滿足兩點(diǎn):
哈希函數(shù)的計算結(jié)果必須是非負(fù)整數(shù),因?yàn)樨?fù)數(shù)無法在數(shù)組中表示。
兩個不同字符串的哈希值盡量不要相等,這樣可以避免在查找時產(chǎn)生沖突。
在Go語言中,字符串的哈希函數(shù)采用的是FNV-1哈希算法,算法代碼如下:
const ( offset64 = 14695981039346656037 prime64 = 1099511628211 ) func stringHash(s string) uint64 { h := uint64(offset64) for i := 0; i < len(s); i++ { h ^= uint64(s[i]) h *= prime64 } return h }
哈希沖突
在哈希表中,哈希值相同的多個字符串可能會存儲在同一個位置上,這種現(xiàn)象叫做哈希沖突。哈希沖突處理策略有開放尋址法、再哈希法和鏈地址法。
開放尋址法:將發(fā)生沖突的條目逐個檢索新的空棑直到找到一個空位置來存儲當(dāng)前鍵值對
再哈希法:對于發(fā)生沖突的鍵,用另一個不同的哈希函數(shù)計算地址
鏈地址法:對于發(fā)生沖突的鍵,將其存儲在一個鏈表中
Go語言使用鏈地址法處理哈希沖突。對于每個存儲單元,map結(jié)構(gòu)體中還維護(hù)了一個[]keyValue
類型的鏈表。
type hmap struct { count int // 映射中的鍵值對數(shù)量 flags uint8 // 控制哈希表的一些屬性 B uint8 // 用于計算哈希地址的初始大小 noverflow uint16 // 鏈表上的溢出桶的數(shù)量 }
Growing
在Go語言中,動態(tài)數(shù)組會自動地為map分配更多的空間。Growing過程涉及到將原始的數(shù)組重新復(fù)制到一個更大的數(shù)組中,其中原數(shù)組的元素需要重新計算其在新數(shù)組中的位置,而新數(shù)組的元素則需要將其鍵值對填充到相應(yīng)的位置。Growing的過程比較復(fù)雜,可以由函數(shù)hashGrow()
來控制。
// hashGrow() 將map的數(shù)組的大小翻倍,并處理哈希沖突。 func hashGrow(h *hmap) { // ... buf := make([]keyValue, newCap) //... for i := uintptr(0); i < cap; i++ { // ... evacuate(h, &h.oldbuckets[i], &buf) // ... } // ... } // evacuate() 將一個bucket中的鍵值對重新映射到新的數(shù)組中 func evacuate(h *hmap, oldbuck *bucket, newbuck *[]keyValue) { // ... }
map擴(kuò)容
雙倍擴(kuò)容
Go語言中的哈希表在map的數(shù)組容量達(dá)到一定程度時,就會自動進(jìn)行擴(kuò)容。擴(kuò)容的依據(jù)是當(dāng)前已存儲的元素數(shù)量和數(shù)組的長度之間的比值:
當(dāng)map的已存儲元素數(shù)量小于map數(shù)組長度的一半時,元素的數(shù)量未達(dá)到哈希表效率的最大值,無需擴(kuò)容;
當(dāng)map已存儲的元素數(shù)量大于等于map數(shù)組長度的一半時,哈希表的查找效率已達(dá)到最大值,所以需要擴(kuò)容。
Go語言的map會優(yōu)先選擇數(shù)組大小為原數(shù)組大小的2倍,以確保map在存儲過程中有足夠的空間存放新的元素。當(dāng)元素數(shù)量達(dá)到85%時,Go語言就會再次對數(shù)組進(jìn)行擴(kuò)容,此時數(shù)組長度翻倍,以保證數(shù)組長度和元素數(shù)量的比例始終維持在0.75左右,以平衡效率和空間占用。
Growing過程
當(dāng)映射中的元素數(shù)量超過85%時,Go語言就會觸發(fā)map的擴(kuò)容過程。在擴(kuò)容的過程中,map會將原有的元素復(fù)制到新的數(shù)組中,并將新數(shù)組的初始大小設(shè)置為原數(shù)組的2倍。對于發(fā)生哈希沖突的元素,需要在新的數(shù)組中重新計算哈希地址。
避免溢出
當(dāng)數(shù)組中元素的數(shù)量超過0x7fffffff
(2^31-1,即int類型的最大值)時,就會發(fā)生溢出,此時數(shù)組的大小將無法達(dá)到原數(shù)組的2倍。所以Go語言會在初始創(chuàng)建map時,為其初始化一個較小的數(shù)組,并設(shè)置map的B值,以便在元素數(shù)量超過限制時再次進(jìn)行擴(kuò)容。當(dāng)map中元素的數(shù)量超過閾值時,會再次翻倍,直到數(shù)組大小小于0x7fffffff
為止。
代碼分析
hashmap.go
包含在Go語言源碼中的src/container/map.go
文件中。其中map
結(jié)構(gòu)體的定義和Growing實(shí)現(xiàn)都在runtime
包中,在src/runtime/map.go
文件中。
附錄
為什么哈希表的容量要設(shè)置為2的n次冪?為什么不是其他數(shù)字?
Go語言中的map是如何進(jìn)行線程安全的?原理是什么?
map的數(shù)據(jù)結(jié)構(gòu)是怎樣的?如何實(shí)現(xiàn)鍵值對的查找、添加、刪除操作?
如何實(shí)現(xiàn)Growing過程?
為什么map的擴(kuò)容條件是85%,而不是100%?
在go語言中如何創(chuàng)建map?
為什么哈希沖突處理策略有開放尋址法、再哈希法和鏈地址法?
如果存在沖突,鍵值對是如何存儲在數(shù)組中的?
為什么Growing過程中會創(chuàng)建一個較大的臨時數(shù)組,而不是直接在原數(shù)組上擴(kuò)展空間?
如何實(shí)現(xiàn)map的迭代?
總結(jié)
本節(jié)我們學(xué)習(xí)了Go語言中的map數(shù)據(jù)類型,使用方法以及map的數(shù)據(jù)安全問題。
到此這篇關(guān)于Golang Map簡介以及底層原理的文章就介紹到這了,更多相關(guān)Golang Map詳解內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang字符串拼接實(shí)現(xiàn)方式和區(qū)別對比
本文介紹了Go語言中字符串拼接的多種方法及其優(yōu)缺點(diǎn),推薦使用strings.Builder進(jìn)行頻繁拼接以優(yōu)化內(nèi)存分配和性能,同時,還討論了通過sync.Pool優(yōu)化高頻創(chuàng)建的對象,以減少垃圾回收壓力,感興趣的朋友一起看看吧2025-02-02golang中結(jié)構(gòu)體嵌套接口的實(shí)現(xiàn)
本文主要介紹了golang中結(jié)構(gòu)體嵌套接口的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2023-04-04最新版Golang?pprof使用詳解(引入、抓取、分析,圖文結(jié)合)
這篇文章主要介紹了最新版Golang?pprof使用詳解包括引入、抓取、分析,圖文結(jié)合,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友參考下吧2024-08-08詳解golang中bufio包的實(shí)現(xiàn)原理
這篇文章主要介紹了詳解golang中bufio包的實(shí)現(xiàn)原理,通過分析golang中bufio包的源碼,來了解為什么bufio能夠提高文件讀寫的效率和速度2018-01-01Go語言通過WaitGroup實(shí)現(xiàn)控制并發(fā)的示例詳解
Channel能夠很好的幫助我們控制并發(fā),但是在開發(fā)習(xí)慣上與顯示的表達(dá)不太相同,所以在Go語言中可以利用sync包中的WaitGroup實(shí)現(xiàn)并發(fā)控制,本文就來和大家詳細(xì)聊聊WaitGroup如何實(shí)現(xiàn)控制并發(fā)2023-01-01golang使用OpenTelemetry實(shí)現(xiàn)跨服務(wù)全鏈路追蹤詳解
這篇文章主要為大家介紹了golang使用OpenTelemetry實(shí)現(xiàn)跨服務(wù)全鏈路追蹤詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09