map的操作和約束
Go語言的字典型別其實就是一個雜湊表的特定實現。字典的鍵的型別是受限的,元素的型別可以是任意型別。
字典的鍵為什麼受限呢?
典的
鍵-元素對
的增刪改查的操作,就是雜湊表的對映過程。
以查詢為例:
- 在雜湊表中查詢與某個鍵值對應的元素值的時候,需要先把鍵值作為引數傳給這個雜湊表。雜湊表用雜湊函式把鍵值轉換為雜湊值。
雜湊值通常是一個無符號的整數。一個雜湊表會持有一定數量的雜湊桶。
- 用這個鍵的雜湊值的低幾位去定位到一個雜湊桶,然後再去這個雜湊桶中查詢這個鍵。
由於鍵-元素對總是被捆綁在一起儲存的,所以一旦找到了鍵,就一定能找到對應的元素值。
- 應的元素值之後,就會將元素值作為結果返回。
我們已經知道了,對映的第一步是把鍵值轉換成雜湊值。那麼字典的鍵不能死哪些型別呢?
回答:字典的鍵不能是函式型別,字典型別,切片型別。
那麼,剩下的:基本資料型別,(int系列,float系列,string,複數),陣列,結構體,指標,介面等都可以作為鍵型別。
問題解析
Go語言規範規定:在鍵型別的值之間必須可以施加操作符==
和!=
。也就是說鍵型別的值必須要支援判等操作。
注意:
- 鍵型別是介面型別時,鍵值的實際型別也不能是上述三種類型。否則會在程式執行時引發panic。
- 最好不要把字典的鍵型別設定為任何介面型別。
- 如果鍵型別是陣列型別,要確保該型別的元素型別不是函式型別,字典型別或切片型別
- 如果鍵型別是結構體型別,也要保證其中欄位的型別的合法性。不合法的型別被埋藏的多深,都會被Go語言編譯器揪出來的。
引申:優先考慮哪些型別作為字典的鍵型別?
從效能角度
求雜湊和判等的速度越快,對應的型別就越適合作為鍵型別。
寬度越小的型別求雜湊的速度通常越快。比如布林型別,整數型別,浮點數型別,複數型別和指標型別。
對於字串型別,由於它的寬度是不定的,所以要看它的值的具體長度,長度越短,求雜湊越快
型別的寬度是指它的單個值需要佔用的位元組數。比如,bool,int8,uint8型別的一個值需要佔用的位元組都是1個。因此寬度就都是1。
對於高階型別:
陣列:對陣列求雜湊實際上是依次求得它的每個元素的雜湊值並進行合併,所以速度就取決於它的元素型別以及它的長度。
結構體:對結構體型別的值求雜湊值實際上就是對它的所有欄位求雜湊值並進行合併。所以關鍵在於它的各個欄位的型別和欄位的數量。
介面:對於介面型別,具體的雜湊演算法由值的實際型別決定。
結論
不建議使用高階資料型別作為字典的鍵型別。優先選用數值型別和指標型別。通常情況下型別的寬度越小越好。如果非要選擇字串型別的話,最好對鍵值的長度進行額外的約束。
一個是因為對他們求雜湊值和判等的速度較慢,還因為他們的值中存在變數。比如,改變陣列中任意一個元素,陣列的雜湊值就變了。
雖然結構體可以通過控制其中欄位的訪問許可權,來防止外界修改它。
把介面型別作為鍵型別最危險。
在值為nil的字典上執行讀操作會成功嗎?那寫操作呢?
由於字典是引用型別,當我們僅僅宣告而不初始化一個字典型別的變數的時候,這個變數的值就是nil。
對這個值為nil的字典進行新增鍵-元素對,會引發錯誤。其他操作沒有問題。
字典型別的值是併發安全的嗎?
非原子操作需要加鎖,map操作不是併發安全的,map併發讀寫需要加鎖。
判斷一個操作是否是原子的,可以使用go run race
命令做資料的競爭檢測。通過 sync.Map 或自己使用sync.RWMutex自己實現併發互斥邏輯