Go語言針對Map的11問你知道幾個?
1. Map 使用時需要注意哪些問題?
- Map 的鍵必須是可比較的類型,如整數(shù)、字符串和指針等,但是切片、函數(shù)和結構體等類型是不可比較的,因此不能用作鍵。
- Map 中的元素是無序的,這意味著遍歷 Map 時,元素的順序可能會隨機改變。
- Map 的容量是動態(tài)變化的,它會自動調(diào)整容量以適應新的元素。
- 如果使用未初始化的 Map,會導致運行時錯誤。需要使用 make() 函數(shù)來初始化 Map。
- Map 在并發(fā)環(huán)境下不是安全的。如果需要在并發(fā)環(huán)境下使用 Map,需要使用 sync 包中提供的鎖機制來保護 Map。
2. Map 擴容是怎么實現(xiàn)的?
在 Go 中,Map 的內(nèi)部實現(xiàn)是基于哈希表(hash table)的,因此,當 Map 中的鍵值對數(shù)量增加時,Map 會自動擴容以提供更多的存儲空間。下面是 Go Map 擴容的具體步驟:
- 當 Map 中的元素數(shù)量超過了負載因子(load factor)和哈希表容量的乘積時,map 就會觸發(fā)擴容操作。在 Go 中,負載因子默認為 6.5。
- Go Map 在擴容時會創(chuàng)建一個新的哈希表,并將原來的鍵值對重新散列到新的哈希表中。為了減少哈希沖突,新哈希表的容量是原來的兩倍,并且容量一定是 2 的冪次方。
- 在重新散列過程中,Go Map 會根據(jù)哈希值將原來的鍵值對分配到新哈希表中的對應位置上。如果兩個鍵值對的哈希值相同,會使用鏈式哈希表(chained hash table)的方式進行處理,將它們放在同一個桶(bucket)中。
- 一旦所有的鍵值對都已經(jīng)重新散列到新的哈希表中,Go Map 就會將原來的哈希表釋放掉,將新的哈希表作為 Map 的內(nèi)部存儲結構。
注意: 由于擴容操作可能會導致大量的重新散列操作,因此在擴容時可能會造成一定的性能影響。為了避免頻繁擴容,可以在創(chuàng)建 Map 時指定一個較大的容量,或者手動調(diào)用 runtime.GC() 來觸發(fā)垃圾回收操作,以釋放未使用的內(nèi)存。
3. Map 的 panic 能被 recover 嗎?
不能,并發(fā)讀寫 Map 也是很容易遇到的問題。如下代碼可以驗證:
package main import ( "fmt" ) func foo(){ defer func(){ if err := recover(); err != nil { fmt.Println(err) } }() var bar = make(map[int]int) go func(){ defer func(){ if err := recover(); err != nil { fmt.Println(err) } }() for{ _ = bar[1] } }() for{ bar[1]=1 } } func main(){ foo() fmt.Println("exit") }
輸出:
fatal error: concurrent map read and map write
goroutine 18 [running]:
runtime.throw({0x1021bdf62?, 0x0?})
/opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/panic.go:992 +0x50 fp=0x14000046f70 sp=0x14000046f40 pc=0x10215f050
runtime.mapaccess1_fast64(0x0?, 0x0?, 0x1)
/opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/map_fast64.go:22 +0x174 fp=0x14000046f90 sp=0x14000046f70 pc=0x10213eaa4
main.foo.func2()
/Users/xxx/go/learn/main.go:43 +0x50 fp=0x14000046fd0 sp=0x14000046f90 pc=0x1021b8a00
runtime.goexit()
/opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/asm_arm64.s:1270 +0x4 fp=0x14000046fd0 sp=0x14000046fd0 pc=0x10218aa64
created by main.foo
/Users/xxx/go/learn/main.go:36 +0x84
goroutine 1 [runnable]:
main.foo()
/Users/xxx/go/learn/main.go:47 +0x98
main.main()
/Users/xxx/go/learn/main.go:52 +0x20
exit status 2
輸出日志里沒有出現(xiàn)我們在程序末尾打印的 exit
,而是直接將調(diào)用棧打印出來了。查看底層 /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/map_fast64.go:22
中的代碼不難發(fā)現(xiàn)這幾行:
if h.flags&hashWriting != 0 { throw("concurrent map read and map write") }
小結一下:
- map會檢測是否存在并發(fā)寫
- 如果檢測到并發(fā)寫會調(diào)用
runtime.throw()
函數(shù)拋出異常,這種異常是無法在業(yè)務代碼中通過recover
捕獲的,直接error
。這點最為致命。 - 如果要并發(fā)寫 map 必須在業(yè)務層面上加鎖(
sync.Mutex
或sync.RWMutext
)或使用sync.Map
4. 并發(fā)使用 Map 除了加鎖還有什么其他方案嗎?
除了加鎖之外,Go 并發(fā)使用 Map 的其他常見解決方案包括使用 sync.Map 和使用并發(fā)安全的第三方 Map 庫。
1、使用 sync.Map sync.Map 是 Go 1.9 新增的一種并發(fā)安全的 Map,它的讀寫操作都是并發(fā)安全的,無需加鎖。使用 sync.Map 的示例代碼如下:
var m sync.Map m.Store("name", "程序員祝融") value, ok := m.Load("name") if ok { fmt.Println(value) } // 輸出程序員祝融
2、使用并發(fā)安全的第三方 Map 庫 除了使用 sync.Map,還可以使用其他第三方的并發(fā)安全的 Map 庫,如 concurrent-map、ccmap 等。這些庫的使用方式與 Go 標準庫的 Map 類似,但它們都提供了更高效、更可靠的并發(fā)安全保證。
注意: 使用并發(fā)安全的第三方 Map 庫可能會導致額外的依賴和復雜性,因此需要仔細評估是否值得使用。
5. sync.Map 和加鎖的區(qū)別是什么?
- sync.Map 和使用鎖的區(qū)別在于,sync.Map 不需要在并發(fā)訪問時進行加鎖和解鎖操作。相比之下,使用鎖需要在并發(fā)訪問時顯式地加鎖和解鎖,以避免競爭條件和數(shù)據(jù)競爭問題。
- 在使用鎖時,當多個 goroutine 同時訪問同一塊數(shù)據(jù)時,必須通過加鎖來避免競爭條件。這意味著只有一個 goroutine 能夠訪問該數(shù)據(jù),并且在該 goroutine 完成工作后,其他 goroutine 才能夠訪問數(shù)據(jù)。這種方式可以確保數(shù)據(jù)的一致性,但是加鎖會帶來額外的開銷,并且在高并發(fā)情況下可能會影響性能。
- 相比之下,sync.Map 使用了更高級的算法來避免競爭條件和數(shù)據(jù)競爭問題,而不需要顯式地進行加鎖和解鎖。當多個 goroutine 同時訪問 sync.Map 時,它會自動分配不同的段來存儲數(shù)據(jù),并且每個段都有自己的讀寫鎖,以避免競爭條件。這種方式可以提高并發(fā)性能,減少開銷,并且避免死鎖等問題。
因此,當需要在并發(fā)訪問時安全地共享數(shù)據(jù)時,可以考慮使用 sync.Map
來避免競爭條件和數(shù)據(jù)競爭問題,并提高性能。而當需要在使用鎖時,需要顯式地加鎖和解鎖來保證數(shù)據(jù)一致性和避免競爭條件。
6. Map 的查詢時間復雜度?
Go 中的 Map 底層實現(xiàn)采用了哈希表,因此其查詢時間復雜度為 O(1),最壞情況為 O(n)。
7.Map Rehash 的策略是怎樣的?什么時機會發(fā)生 Rehash?
當哈希表中的元素數(shù)量達到一定閾值時,就會觸發(fā)哈希表的擴容操作,也就是進行 rehash。
Go 標準庫中的哈希表實現(xiàn)在以下情況下會觸發(fā) rehash 操作:
- 當哈希表中的元素數(shù)量超過了哈希表容量的 2/3 時,會觸發(fā)擴容操作。此時,哈希表的容量會翻倍,并且哈希表中所有的元素都會重新哈希到新的槽位中。
- 當哈希表中的元素數(shù)量過少,而哈希表的容量過大時,會觸發(fā)收縮操作。此時,哈希表的容量會減半,并且哈希表中所有的元素都會重新哈希到新的槽位中。
- 當哈希表的探測序列過長時,會觸發(fā)重排操作。此時,哈希表中的元素不會重新哈希,但是它們的存儲位置會發(fā)生變化,從而減少聚簇現(xiàn)象,提高哈希表的性能。
在進行 rehash 操作時,哈希表會創(chuàng)建一個新的數(shù)組來存儲重新哈希后的元素,然后將舊數(shù)組中的元素逐個復制到新數(shù)組中。由于重新哈希的過程比較耗時,因此 Go 標準庫中的哈希表實現(xiàn)采用了增量式 rehash 策略,在擴容和收縮時只會處理一部分元素,避免一次性處理過多元素導致性能下降。具體來說,增量式 rehash 的策略是:
- 將新數(shù)組的容量設置為舊數(shù)組的兩倍或一半,并且將哈希表的增量計數(shù)器加一。
- 在對哈希表進行操作時,如果發(fā)現(xiàn)增量計數(shù)器的值達到了一個閾值,就會開始進行增量式 rehash 操作,將一部分元素從舊數(shù)組中復制到新數(shù)組中,并且重新計算這些元素的哈希值。
- 在完成一次增量式 rehash 操作后,會將哈希表的增量計數(shù)器清零。
通過增量式 rehash 的策略,Go 標準庫中的哈希表實現(xiàn)可以在保證較短的 rehash 時間的同時,避免一次性處理過多元素導致性能下降。
8. Map Rehash 具體會影響什么?哈希結果會受到什么影響?
rehash 操作會影響 Map
的性能。由于重新計算鍵的哈希值,rehash 操作會消耗一定的計算資源。此外,在 rehash 過程中,原始哈希表的所有鍵值對都需要復制到新的哈希表中,因此 rehash 操作還會消耗一定的內(nèi)存空間和時間。
rehash 操作不會直接影響哈希結果。哈希結果是由哈希函數(shù)計算得出的,與 Map
中元素的數(shù)量和布局無關。rehash 操作只會影響哈希表的布局,即每個鍵在哈希表中的位置會發(fā)生變化,但是每個鍵的哈希值并不會改變。
9. Map Rehash 過程中存放在舊桶的元素如何遷移?
rehash 操作會創(chuàng)建一個新的哈希表,同時保留舊的哈希表不變。然后,它會依次遍歷舊哈希表中的每個桶,將桶中的所有鍵值對復制到新哈希表中對應的桶中。在遍歷每個桶時,如果桶中的元素已經(jīng)被刪除,則跳過這些元素。
當遍歷到一個桶時,Map
實現(xiàn)會首先獲取舊哈希表中該桶的互斥鎖,以確保在復制元素時不會有并發(fā)訪問的問題。然后,它會遍歷桶中的所有鍵值對,對于每個鍵值對,它會計算鍵在新哈希表中的位置,并將鍵值對插入到對應的桶中。插入過程中,如果新哈希表中對應的桶已經(jīng)存在元素,則會將新元素插入到該桶的鏈表的尾部。
在整個 rehash 過程中,新哈希表會保持為空,直到所有元素都被復制完畢。復制完成后,Map
實現(xiàn)會用新哈希表替換舊哈希表,并釋放舊哈希表占用的內(nèi)存空間。這樣,Map
中的所有元素都被遷移到了新哈希表中。
需要注意的是,在 rehash 過程中,如果有并發(fā)訪問 Map
,則可能會發(fā)生競爭條件。因此,Go 語言中的 Map
不是線程安全的。如果需要在多個 goroutine 中訪問 Map
,則需要使用互斥鎖或其他并發(fā)安全的機制來保護訪問。
10. sync.Map 的 Load() 方法流程?
sync.Map
中的 Load()
方法用于獲取指定鍵對應的值,其流程如下:
Load()
方法首先會獲取sync.Map
內(nèi)部的讀鎖,以確保在訪問map
中的數(shù)據(jù)時不會被其他 goroutine 干擾。如果此時有其他 goroutine 正在寫入sync.Map
,則Load()
方法會阻塞,直到寫入操作完成。Load()
方法接著會在sync.Map
中查找指定的鍵,并返回其對應的值。如果指定鍵不存在,則Load()
方法會返回一個零值和一個布爾值,表示該鍵不存在。Load()
方法完成后,會釋放sync.Map
的讀鎖,以便其他 goroutine 可以讀取或?qū)懭?nbsp;map
。
注意:雖然 sync.Map
可以在多個 goroutine 中安全地訪問和修改,但由于其內(nèi)部仍然使用鎖來保證并發(fā)安全,因此在高并發(fā)場景下,其性能可能不如非并發(fā)安全的 map
類型。因此,在并發(fā)性能要求較高的場景下,建議使用更為輕量級的并發(fā)控制機制,例如互斥鎖和讀寫鎖, 來保護普通的 map
。
11. sync.Map Store() 如何保持緩存層和底層 Map 數(shù)據(jù)是相同的?
在 Go 語言的 sync.Map
中,Store()
方法用于向映射中存儲一個鍵值對。為了保持緩存層和底層 map
數(shù)據(jù)的一致性,sync.Map
使用了一種特殊的機制,具體流程如下:
- 當使用
Store()
方法向sync.Map
存儲一個鍵值對時,首先會將鍵和值打包成一個interface{}
類型的元素,并將該元素存儲在一個map
類型的緩存層中。 - 當緩存層中的元素數(shù)量達到一定閾值(默認為256)時,
sync.Map
會將緩存層中的所有元素都復制到一個新的底層map
中,并將原來的底層map
替換為新的底層map
。 - 在底層
map
替換過程中,sync.Map
會先獲取一個寫鎖,以確保沒有其他 goroutine 在此期間對底層map
進行讀寫操作。 sync.Map
將緩存層中的所有元素復制到新的底層map
中,并在底層map
上調(diào)用Store()
方法來存儲這些元素。這樣,所有新的元素都被添加到新的底層map
中,而原來的底層map
中的元素則被保留下來。- 當新的底層
map
中的元素添加完成后,sync.Map
會釋放寫鎖,并將緩存層中的元素清空。這樣,緩存層和底層map
的數(shù)據(jù)就保持了一致性。
注意: sync.Map
使用緩存層和底層 map
之間的轉換機制來避免鎖的使用,從而提高了并發(fā)性能。然而,由于緩存層和底層 map
之間存在一定的延遲和不一致性,因此在一些特殊的場景下,可能會出現(xiàn)一些數(shù)據(jù)同步的問題,需要特別注意。
到此這篇關于Go語言針對Map的11問你知道幾個?的文章就介紹到這了,更多相關Go語言Map內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Go語言中io.Reader和io.Writer的詳解與實現(xiàn)
在Go語言的實際編程中,幾乎所有的數(shù)據(jù)結構都圍繞接口展開,接口是Go語言中所有數(shù)據(jù)結構的核心。在使用Go語言的過程中,無論你是實現(xiàn)web應用程序,還是控制臺輸入輸出,又或者是網(wǎng)絡操作,不可避免的會遇到IO操作,使用到io.Reader和io.Writer接口。下面來詳細看看。2016-09-09