十個Go map面試??紗栴}合集
1.Map 使用時需要注意哪些問題
Map 的鍵必須是可比較的類型,如整數、字符串和指針等,但是切片、函數和結構體等類型是不可比較的,因此不能用作鍵。
Map 中的元素是無序的,這意味著遍歷 Map 時,元素的順序可能會隨機改變。
Map 的容量是動態(tài)變化的,它會自動調整容量以適應新的元素。
如果使用未初始化的 Map,會導致運行時錯誤。需要使用 make() 函數來初始化 Map。
Map 在并發(fā)環(huán)境下不是安全的。如果需要在并發(fā)環(huán)境下使用 Map,需要使用 sync 包中提供的鎖機制來保護 Map。
2.Map 擴容是怎么實現的
在 Go 中,Map 的內部實現是基于哈希表(hash table)的,因此,當 Map 中的鍵值對數量增加時,Map 會自動擴容以提供更多的存儲空間。下面是 Go Map 擴容的具體步驟:
- 當 Map 中的元素數量超過了負載因子(load factor)和哈希表容量的乘積時,map 就會觸發(fā)擴容操作。在 Go 中,負載因子默認為 6.5。
- Go Map 在擴容時會創(chuàng)建一個新的哈希表,并將原來的鍵值對重新散列到新的哈希表中。為了減少哈希沖突,新哈希表的容量是原來的兩倍,并且容量一定是 2 的冪次方。即每次擴容按照規(guī)則計算出新容量之后,為了內存對齊,減少內存碎片,會根據一個常量數字進行向上取整。
- 在重新散列過程中,Go Map 會根據哈希值將原來的鍵值對分配到新哈希表中的對應位置上。如果兩個鍵值對的哈希值相同,會使用鏈式哈希表(chained hash table)的方式進行處理,將它們放在同一個桶(bucket)中。
- 一旦所有的鍵值對都已經重新散列到新的哈希表中,Go Map 就會將原來的哈希表釋放掉,將新的哈希表作為 Map 的內部存儲結構。
注意: 由于擴容操作可能會導致大量的重新散列操作,因此在擴容時可能會造成一定的性能影響。為了避免頻繁擴容,可以在創(chuàng)建 Map 時指定一個較大的容量,或者手動調用 runtime.GC() 來觸發(fā)垃圾回收操作,以釋放未使用的內存。
3. Map 的 panic 能被 recover 嗎
不能,并發(fā)讀寫 Map 也是很容易遇到的問題
if?h.flags&hashWriting?!=?0?{ ??throw("concurrent?map?read?and?map?write") }
runtime.throw()
函數拋出異常是無法recover的
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ā)訪問時顯式地加鎖和解鎖,以避免競爭條件和數據競爭問題。
在使用鎖時,當多個 goroutine 同時訪問同一塊數據時,必須通過加鎖來避免競爭條件。這意味著只有一個 goroutine 能夠訪問該數據,并且在該 goroutine 完成工作后,其他 goroutine 才能夠訪問數據。這種方式可以確保數據的一致性,但是加鎖會帶來額外的開銷,并且在高并發(fā)情況下可能會影響性能。
相比之下,sync.Map 使用了更高級的算法來避免競爭條件和數據競爭問題,而不需要顯式地進行加鎖和解鎖。當多個 goroutine 同時訪問 sync.Map 時,它會自動分配不同的段來存儲數據,并且每個段都有自己的讀寫鎖,以避免競爭條件。這種方式可以提高并發(fā)性能,減少開銷,并且避免死鎖等問題。
6. Map 的查詢時間復雜度
Go 中的 Map 底層實現采用了哈希表,因此其查詢時間復雜度為 O(1),最壞情況為 O(n)。
7. Map Rehash 的策略是怎樣的 什么時機會發(fā)生 Rehash
當哈希表中的元素數量達到一定閾值時,就會觸發(fā)哈希表的擴容操作,也就是進行 rehash。
Go 標準庫中的哈希表實現在以下情況下會觸發(fā) rehash 操作:
- 當哈希表中的元素數量超過了哈希表容量的 2/3 時,會觸發(fā)擴容操作。此時,哈希表的容量會翻倍,并且哈希表中所有的元素都會重新哈希到新的槽位中。
- 當哈希表中的元素數量過少,而哈希表的容量過大時,會觸發(fā)收縮操作。此時,哈希表的容量會減半,并且哈希表中所有的元素都會重新哈希到新的槽位中。
- 當哈希表的探測序列過長時,會觸發(fā)重排操作。此時,哈希表中的元素不會重新哈希,但是它們的存儲位置會發(fā)生變化,從而減少聚簇現象,提高哈希表的性能。
在進行 rehash 操作時,哈希表會創(chuàng)建一個新的數組來存儲重新哈希后的元素,然后將舊數組中的元素逐個復制到新數組中。由于重新哈希的過程比較耗時,因此 Go 標準庫中的哈希表實現采用了增量式 rehash 策略,在擴容和收縮時只會處理一部分元素,避免一次性處理過多元素導致性能下降。具體來說,增量式 rehash 的策略是:
- 將新數組的容量設置為舊數組的兩倍或一半,并且將哈希表的增量計數器加一。
- 在對哈希表進行操作時,如果發(fā)現增量計數器的值達到了一個閾值,就會開始進行增量式 rehash 操作,將一部分元素從舊數組中復制到新數組中,并且重新計算這些元素的哈希值。
- 在完成一次增量式 rehash 操作后,會將哈希表的增量計數器清零。
通過增量式 rehash 的策略,Go 標準庫中的哈希表實現可以在保證較短的 rehash 時間的同時,避免一次性處理過多元素導致性能下降
8. Map Rehash 具體會影響什么?哈希結果會受到什么影響
rehash 操作會影響 Map
的性能。由于重新計算鍵的哈希值,rehash 操作會消耗一定的計算資源。此外,在 rehash 過程中,原始哈希表的所有鍵值對都需要復制到新的哈希表中,因此 rehash 操作還會消耗一定的內存空間和時間。
rehash 操作不會直接影響哈希結果。哈希結果是由哈希函數計算得出的,與 Map
中元素的數量和布局無關。rehash 操作只會影響哈希表的布局,即每個鍵在哈希表中的位置會發(fā)生變化,但是每個鍵的哈希值并不會改變。
9. Map Rehash 過程中存放在舊桶的元素如何遷移
rehash 操作會創(chuàng)建一個新的哈希表,同時保留舊的哈希表不變。然后,它會依次遍歷舊哈希表中的每個桶,將桶中的所有鍵值對復制到新哈希表中對應的桶中。在遍歷每個桶時,如果桶中的元素已經被刪除,則跳過這些元素。
當遍歷到一個桶時,Map
實現會首先獲取舊哈希表中該桶的互斥鎖,以確保在復制元素時不會有并發(fā)訪問的問題。然后,它會遍歷桶中的所有鍵值對,對于每個鍵值對,它會計算鍵在新哈希表中的位置,并將鍵值對插入到對應的桶中。插入過程中,如果新哈希表中對應的桶已經存在元素,則會將新元素插入到該桶的鏈表的尾部。
在整個 rehash 過程中,新哈希表會保持為空,直到所有元素都被復制完畢。復制完成后,Map
實現會用新哈希表替換舊哈希表,并釋放舊哈希表占用的內存空間。這樣,Map
中的所有元素都被遷移到了新哈希表中。
需要注意的是,在 rehash 過程中,如果有并發(fā)訪問 Map
,則可能會發(fā)生競爭條件。因此,Go 語言中的 Map
不是線程安全的。如果需要在多個 goroutine 中訪問 Map
,則需要使用互斥鎖或其他并發(fā)安全的機制來保護訪問
10. sync.Map 的 Load() 方法流程
func (m *Map) Load(key any) (value any, ok bool) { read, _ := m.read.Load().(readOnly) e, ok := read.m[key] if !ok && read.amended { m.mu.Lock() // Avoid reporting a spurious miss if m.dirty got promoted while we were // blocked on m.mu. (If further loads of the same key will not miss, it's // not worth copying the dirty map for this key.) read, _ = m.read.Load().(readOnly) e, ok = read.m[key] if !ok && read.amended { e, ok = m.dirty[key] // Regardless of whether the entry was present, record a miss: this key // will take the slow path until the dirty map is promoted to the read // map. m.missLocked() } m.mu.Unlock() } if !ok { return nil, false } return e.load() }
可見Load
函數直接從底層讀取元素,如果存在則返回,不管此時是否有寫入操作。因為e.load
函數內是通過原子操作讀取數據的。
因此當key存在的時候,Load
函數不會加鎖。Stone
當元素存在的時候也不會加鎖,而是通過atomic.CompareAndSwapPointer
修改元素的值。
所以Load
,Store
只有當key不存在的時候才會加鎖,其他情況不會加鎖,因此它適合讀多寫少的場景,速度非常快。
到此這篇關于十個Go map面試??紗栴}合集的文章就介紹到這了,更多相關Go map內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Apache?IoTDB開發(fā)系統(tǒng)之Go原生接口方法
這篇文章主要為大家介紹了?Apache?IoTDB開發(fā)系統(tǒng)之Go原生接口方法,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-09-09