詳解Golang中使用map時的注意問題
1. 將value定義為struct節(jié)省內(nèi)存
1. 消除指針引用
當 map
的 value 是 struct 類型時,數(shù)據(jù)會直接存儲在 map 中,而不是通過指針引用。這可以減少內(nèi)存分配的開銷和 GC(垃圾回收)的負擔。
type User struct { ID int Name string } m := make(map[string]User) m["user1"] = User{ID: 1, Name: "John"} // Example with pointer to struct m2 := make(map[string]*User) m2["user1"] = &User{ID: 1, Name: "John"}
在第二個示例中,map 中存儲的是指向 User
結(jié)構(gòu)體的指針,這意味著除了存儲指針本身外,還需要額外的內(nèi)存來存儲 User
結(jié)構(gòu)體,并且會增加 GC 的負擔。
2. 避免內(nèi)存碎片化
存儲指針時,由于指針可能指向堆中的不同位置,這會導(dǎo)致內(nèi)存碎片化,增加了內(nèi)存使用的不確定性。而存儲 struct 使得數(shù)據(jù)更緊湊,減少了碎片化。
3. 更高的緩存命中率
由于 struct 的數(shù)據(jù)是緊湊存儲的,相對于存儲指針,struct 的數(shù)據(jù)更可能在相鄰的內(nèi)存位置。這增加了 CPU 緩存的命中率,從而提高了性能。
示例:節(jié)約內(nèi)存
下面是一個示例,展示了如何通過定義 struct 類型來節(jié)約內(nèi)存:
package main import ( "fmt" "runtime" ) type User struct { ID int Name string } func main() { // 使用 struct 作為 value users := make(map[string]User) for i := 0; i < 1000000; i++ { users[fmt.Sprintf("user%d", i)] = User{ID: i, Name: fmt.Sprintf("Name%d", i)} } printMemUsage("With struct values") // 使用指針作為 value userPtrs := make(map[string]*User) for i := 0; i < 1000000; i++ { userPtrs[fmt.Sprintf("user%d", i)] = &User{ID: i, Name: fmt.Sprintf("Name%d", i)} } printMemUsage("With pointer values") } func printMemUsage(label string) { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("%s: Alloc = %v MiB\n", label, bToMb(m.Alloc)) } func bToMb(b uint64) uint64 { return b / 1024 / 1024 }
4. set實現(xiàn)對比
map[int]bool{}
在這種情況下,map 的 value 類型是 bool
。每個鍵會占用一個 bool 類型的空間(通常是一個字節(jié))。
set := make(map[int]bool) set[1] = true set[2] = true
map[int]struct{}{}
在這種情況下,map 的 value 類型是空的 struct??盏?struct 不占用任何內(nèi)存,因此每個鍵只占用鍵本身的內(nèi)存。
set := make(map[int]struct{}) set[1] = struct{}{} set[2] = struct{}{}
內(nèi)存使用對比
map[int]bool{} 會比 map[int]struct{}{} 使用更多的內(nèi)存,因為 bool 類型需要存儲一個字節(jié)(在實際應(yīng)用中可能會有額外的內(nèi)存對齊和管理開銷),而 struct{} 是空的,不會增加任何內(nèi)存開銷。
示例代碼對比內(nèi)存使用
以下是一個示例代碼,比較這兩種 map 類型的內(nèi)存使用情況:
package main import ( "fmt" "runtime" ) func main() { // 使用 bool 作為 value boolMap := make(map[int]bool) for i := 0; i < 1000000; i++ { boolMap[i] = true } printMemUsage("With bool values") // 使用 struct 作為 value structMap := make(map[int]struct{}) for i := 0; i < 1000000; i++ { structMap[i] = struct{}{} } printMemUsage("With struct values") } func printMemUsage(label string) { var m runtime.MemStats runtime.ReadMemStats(&m) fmt.Printf("%s: Alloc = %v MiB\n", label, bToMb(m.Alloc)) } func bToMb(b uint64) uint64 { return b / 1024 / 1024 }
結(jié)果
運行上述代碼,你會發(fā)現(xiàn)使用 struct 作為 value 的內(nèi)存使用量明顯小于使用指針作為 value 的內(nèi)存使用量。這是因為:
- 減少了指針的存儲開銷。
- 減少了額外的堆內(nèi)存分配。
- 降低了 GC 的負擔,因為 struct 的內(nèi)存管理更簡單,不涉及指針的追蹤和回收。
2. 哈希分桶的結(jié)構(gòu)
1. 哈希計算
當我們向map中插入一個鍵值對,首先對鍵進行哈希計算。Go
內(nèi)置了哈希函數(shù)來計算鍵的哈希值。哈希值是一個64
位的整數(shù)。
2. 分桶依據(jù)
Go 中的 map 是分成多個桶 (bucket) 來存儲的。桶的數(shù)量通常是 2 的冪次,這樣可以方便地通過位運算來定位到具體的桶。哈希值的高八位和低八位分別用于分桶和桶內(nèi)定位:
- 高八位 (top 8 bits):用于決定哈希表中的桶位置。
- 低八位 (low 8 bits):用于桶內(nèi)查找。
3. 桶 (Bucket) 結(jié)構(gòu)
每個桶中可以存儲 8 個鍵值對。當某個桶中的元素超過 8 個時,Go 會使用溢出桶來存儲額外的鍵值對。桶的結(jié)構(gòu)如下:
type bmap struct { tophash [bucketCnt]uint8 keys [bucketCnt]keyType values [bucketCnt]valueType overflow *bmap }
tophash:存儲鍵的哈希值的高八位。
keys:存儲鍵。
values:存儲對應(yīng)的值。
overflow:指向溢出桶的指針。
4. 插入過程
當插入一個鍵值對時,過程如下:
- 計算哈希值:對鍵進行哈希計算得到哈希值
hash
。 - 定位桶:通過
hash >> (64 - B)
(B
是桶的數(shù)量的對數(shù))得到桶的索引index
。 - 桶內(nèi)查找:通過
hash & (bucketCnt - 1)
得到桶內(nèi)索引。然后通過對比tophash
數(shù)組中的值來定位到具體的鍵值對存儲位置。 - 存儲鍵值對:將鍵值對存儲到相應(yīng)的位置,如果當前桶已滿,則分配新的溢出桶來存儲額外的鍵值對。
5. 查找過程
查找的過程與插入類似:
查找的過程與插入類似:
- 計算哈希值:對鍵進行哈希計算得到哈希值
hash
。 - 定位桶:通過
hash >> (64 - B)
得到桶的索引index
。 - 桶內(nèi)查找:通過
hash & (bucketCnt - 1)
得到桶內(nèi)索引,然后在相應(yīng)的bmap
中查找tophash
和keys
數(shù)組中匹配的鍵。如果在當前桶中沒有找到,則繼續(xù)查找溢出桶。
3. map擴容過程
1. 擴容觸發(fā)條件
擴容通常在以下兩種情況下觸發(fā):
擴容通常在以下兩種情況下觸發(fā):
- 裝載因子過高:裝載因子(load factor)是 map 中元素數(shù)量與桶數(shù)量的比值。Go 語言中的裝載因子閾值通常為 6.5,當裝載因子超過這個值時會觸發(fā)擴容。
- 溢出桶過多:當溢出桶的數(shù)量過多時,也會觸發(fā)擴容。
2. 擴容過程的具體步驟
- 初始化新的桶數(shù)組: 在需要擴容時,Go 會分配一個新的桶數(shù)組,其大小通常是舊桶數(shù)組的兩倍,并設(shè)置相關(guān)的元數(shù)據(jù)以指示 map 正在進行擴容。
- 標記遷移狀態(tài): 在 map 的內(nèi)部結(jié)構(gòu)中,會有一個標志位(rehash index)指示當前已經(jīng)遷移的桶位置。初始值為 0。
- 遷移部分數(shù)據(jù): 在每次對 map 進行插入或查找操作時,會順便遷移一部分舊桶中的數(shù)據(jù)到新桶中。每次遷移一個或多個桶,具體數(shù)量取決于操作的復(fù)雜度。
- 更新 rehash index: 遷移完成后,更新 rehash index,以便下次操作繼續(xù)遷移下一個桶中的數(shù)據(jù)。
- 完成擴容: 當所有舊桶的數(shù)據(jù)都遷移到新桶后,更新 map 的元數(shù)據(jù),指向新的桶數(shù)組,并將擴容狀態(tài)標志位清除。
4. recover map的panic
panic 和 recover 的工作機制
- panic:
panic
用于引發(fā)一個恐慌,通常在遇到無法恢復(fù)的嚴重錯誤時使用。- 當
panic
被調(diào)用時,程序的正常執(zhí)行流程會被中斷,并開始沿著調(diào)用棧向上展開,逐層調(diào)用函數(shù)的defer
語句,直到遇到recover
或者程序崩潰。
- recover:
recover
用于恢復(fù)程序的正常執(zhí)行,通常在defer
函數(shù)中調(diào)用。- 如果在
defer
語句中調(diào)用了recover
,并且當前棧幀處于恐慌狀態(tài),那么recover
會捕獲這個恐慌,停止棧的展開,并返回傳給panic
的值。 - 如果不在恐慌狀態(tài)下調(diào)用
recover
,它會返回nil
,不做任何處理。
在 Go 語言中,panic
和 recover
是用來處理異常情況和錯誤恢復(fù)的兩種機制。理解它們的工作原理對于編寫健壯的 Go 代碼非常重要。以下是對 panic
和 recover
機制的詳細解釋以及它們在 map
中的應(yīng)用。
panic 和 recover 的工作機制
- panic:
panic
用于引發(fā)一個恐慌,通常在遇到無法恢復(fù)的嚴重錯誤時使用。- 當
panic
被調(diào)用時,程序的正常執(zhí)行流程會被中斷,并開始沿著調(diào)用棧向上展開,逐層調(diào)用函數(shù)的defer
語句,直到遇到recover
或者程序崩潰。
- recover:
recover
用于恢復(fù)程序的正常執(zhí)行,通常在defer
函數(shù)中調(diào)用。- 如果在
defer
語句中調(diào)用了recover
,并且當前棧幀處于恐慌狀態(tài),那么recover
會捕獲這個恐慌,停止棧的展開,并返回傳給panic
的值。 - 如果不在恐慌狀態(tài)下調(diào)用
recover
,它會返回nil
,不做任何處理。
在 map 中使用 panic 和 recover
在 Go 的 map
中,某些操作(如并發(fā)讀寫未加鎖的 map
)會引發(fā) panic
。這些 panic
可以被 recover
捕獲和處理,以防止程序崩潰。
package main import ( "fmt" ) func main() { defer func() { if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) } }() // 創(chuàng)建一個 map m := make(map[string]string) // 引發(fā) panic 的操作 causePanic(m) fmt.Println("This line will be executed because panic was recovered.") } func causePanic(m map[string]string) { // 這里嘗試并發(fā)訪問 map,可能會引發(fā) panic // 模擬并發(fā)問題,直接引發(fā) panic panic("simulated map access panic") }
5. map是如何檢測到自己處于競爭狀態(tài)
在 Go 語言中,map 的競爭狀態(tài)(concurrent access)指的是多個 goroutine 同時讀寫同一個 map 而沒有適當?shù)耐奖Wo。Go 內(nèi)置的 map 類型在并發(fā)讀寫時會引發(fā) panic,以防止數(shù)據(jù)競爭和未定義行為。這種檢測主要是通過 Go 編譯器和運行時的實現(xiàn)來完成的,而不是底層硬件直接支持的功能。
競爭檢測機制
- 編譯器插樁:
- 在編譯時,Go 編譯器會在對 map 進行讀寫操作的代碼位置插入特定的檢測代碼。這些檢測代碼在運行時檢查 map 是否處于并發(fā)訪問狀態(tài)。
- 運行時檢查:
- 運行時的檢測代碼會追蹤 map 的訪問。當檢測到多個 goroutine 同時對 map 進行讀寫操作時,會引發(fā) panic。具體來說,Go 運行時會記錄每個 map 的訪問情況,如果檢測到并發(fā)訪問沒有通過同步機制(如
sync.Mutex
),就會引發(fā) panic。
- 運行時的檢測代碼會追蹤 map 的訪問。當檢測到多個 goroutine 同時對 map 進行讀寫操作時,會引發(fā) panic。具體來說,Go 運行時會記錄每個 map 的訪問情況,如果檢測到并發(fā)訪問沒有通過同步機制(如
package main import ( "fmt" "sync" ) func main() { m := make(map[int]int) var wg sync.WaitGroup var mu sync.Mutex // 啟動多個 goroutine 并發(fā)寫 map,未加鎖保護會引發(fā) panic for i := 0; i < 10; i++ { wg.Add(1) go func(i int) { defer wg.Done() // 取消注釋以下行,查看未加鎖保護的并發(fā)寫操作 // m[i] = i // 使用互斥鎖保護并發(fā)寫操作 mu.Lock() m[i] = i mu.Unlock() }(i) } wg.Wait() // 打印 map 內(nèi)容 mu.Lock() for k, v := range m { fmt.Printf("key: %d, value: %d\n", k, v) } mu.Unlock() }
6. sync.Map和map加鎖的區(qū)別
- 使用場景:
sync.Map
適用于讀多寫少的并發(fā)場景,簡單且高效。- 使用
sync.Mutex
或sync.RWMutex
保護普通 map 適用于需要復(fù)雜并發(fā)控制或?qū)懖僮鬏^多的場景。
- 性能:
sync.Map
在讀多寫少的情況下性能優(yōu)越,但在寫操作頻繁時性能可能不如使用互斥鎖保護的普通 map。- 使用
sync.Mutex
或sync.RWMutex
可以在讀寫操作間提供更好的性能平衡,尤其是在寫操作較多時。
- 復(fù)雜性:
sync.Map
封裝了并發(fā)控制,使用簡單,不需要手動加鎖。- 使用
sync.Mutex
或sync.RWMutex
需要手動加鎖解鎖,代碼相對復(fù)雜,但更靈活。
- 方法支持:
sync.Map
提供了一些特殊的方法(如LoadOrStore
、Range
),方便特定場景下的使用。- 使用
sync.Mutex
或sync.RWMutex
保護的普通 map 可以自由定義自己的方法,更靈活,但需要更多的代碼。
- 使用場景:
以上就是詳解Golang中使用map時的注意問題的詳細內(nèi)容,更多關(guān)于Golang使用map的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
go使用SQLX操作MySQL數(shù)據(jù)庫的教程詳解
sqlx 是 Go 語言中一個流行的操作數(shù)據(jù)庫的第三方包,它提供了對 Go 標準庫 database/sql 的擴展,簡化了操作數(shù)據(jù)庫的步驟,下面我們就來學(xué)習(xí)一下go如何使用SQLX實現(xiàn)MySQL數(shù)據(jù)庫的一些基本操作吧2023-11-11Go語言 channel如何實現(xiàn)歸并排序中的merge函數(shù)詳解
這篇文章主要給大家介紹了關(guān)于Go語言 channel如何實現(xiàn)歸并排序中merge函數(shù)的相關(guān)資料,文中通過示例代碼介紹的非常詳細,對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2018-02-02