Go Map
定義
map底層實現的是是一個hashmap,data會被存儲到一個bucket數組裏,每個bucket最多存8個KV鍵值對。
宣告及創建
var m1 map[string]string // ==> nil map
var m2 map[string]string{} // ==> empty map
var m3 map[string]string{
"key1":"value1",
"key2":"value2",
"key3":"value3",
"key4":"value4"
}
Map遍歷
for k,v := range m3 {
fmt.Println(k,v)
}
map的key是無序的,所以每次遍歷出來的結果也是無序的,因爲底層go的map相當於是一個hash map
Map基本操作
取
Go中map取key對應value的形式就是 map[key],取值的時候通常會有返回一個ok這個參數,表示這個key是否在這個map中存在。如果不存在就如m3[“key6”]會返回false,在Go即使key不在map中也會返回一個宣告的Value的對應初始值,不會報錯。
key1Value,ok := m3["key1"] // ==> "value1",true
key5Value := m3["key5"] // ==> ""
key6Value,ok := m3["key6"] // ==> "",false
刪
delete(m3,"key1")
底層原理
怎麼存?
Go底層實現map,其實是實現一個哈希表,其中最重要的結構體是/src/runtime/map.go中的hmap和bmap結構體。
// map底層實現的結構體
// /src/runtime/map.go
type hmap struct {
count int //元素個數,實現len(),所以可以用len來取map有多少個元素
flags uint8 //狀態標誌
B uint8 // 最多可以容納 裝載因子 * 2 ^ B個元素,B值等於取buckets總數的2的對數
noverflow uint16 // 溢出的bucket個數
hash0 uint32 // 哈希種子
buckets unsafe.Pointer // 指向bucket數組的指針,如果count爲空,則該值爲nil
oldbuckets unsafe.Pointer // 當haspmap需要擴容時,指向舊bucket數組的指針,長度爲舊bucket的一半
nevacuate uintptr // 遷移進度
extra *mapextra // 用於擴容的指針
}
這個結構體裏最重要的就是bucket 數組,因爲Go中map用於存儲的結構體是bucket數組。每個bucket裏面會存8個kv鍵值對,每當bucket的kv鍵值對容量用完之後,會有一個overflow指針指向一個新的bucket,從而形成一個鏈表結構
。下面是bucket的結構體,可以看到bucket這個結構體沒有所謂的key,value,overflow字段,是因爲讀取kv的方式是指針運算
。另外,從註釋內可以看到,kv存儲的方式也和以往的key/value/key/value…的形式不同,採用的是所有key打包在一起,所有value打包在一起的方式,即key/key/key…/value/value…的方式。這樣做的方式雖然比舊有方式運算來得複雜,但是可以消除padding帶來的空間浪費(這個地方是個人理解,會對內存的使用產生影響,但具體怎麼影響還需要去了解下數據在硬盤中的存儲方式)。
// bucket結構體
type bmap struct {
// tophash generally contains the top byte of the hash value
// for each key in this bucket. If tophash[0] < minTopHash,
// tophash[0] is a bucket evacuation state instead.
tophash [bucketCnt]uint8
// Followed by bucketCnt keys and then bucketCnt values.
// NOTE: packing all the keys together and then all the values together makes the
// code a bit more complicated than alternating key/value/key/value/... but it allows
// us to eliminate padding which would be needed for, e.g., map[int64]int8.
// Followed by an overflow pointer.
}
所以從上面兩個結構體,我們可以得知Go Map底層存儲的整體結構如下圖:
怎麼讀?
Go底層通過哈希表實現map,哈希表顧名思義會有一個hash function來計算對應的hash值。Go也一樣,針對map宣告的數據類型不同,用不同的hash function來計算hash值,在後面的源碼中會看到。hash function在/runtime/alg.go裏。
和其他hash function不同的是,Go通過hash function計算出來的值不是直接來當做key,而是將算出來的值分爲高8位和低8位。爲什麼是8,因爲bmap中的tophash定義的數據類型。高8位用來計算屬於bucket中的哪個key,bmap中的tophash存的就是高8位的值,也可以理解爲這個bucket有哪些key。低8位用來定位hmap中的哪個bucket。
下方代碼塊是Go中取value的方法源碼,還有一個mapaccess2會返回一個bool值來表示這個key在這個map中是否存在。
map獲取key對應的value永遠都不會回傳nil值,因爲會有一個zero object用來返回不存在map中的key的value。
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapaccess1)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.key.alg.hash(key, 0)
}
return unsafe.Pointer(&zeroVal[0])
}
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 通過maptype來獲取對應的hash function,算出hash值
alg := t.key.alg
hash := alg.hash(key, uintptr(h.hash0))
m := bucketMask(h.B)
b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize))) //低位計算屬於哪個bucket
if c := h.oldbuckets; c != nil {
if !h.sameSizeGrow() {
m >>= 1
}
oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
if !evacuated(oldb) {
b = oldb
}
}
top := tophash(hash) //高位計算bucket中對應的key
bucketloop:
for ; b != nil; b = b.overflow(t) { //當前bucket找不到對應的key去overflow指向的bucket找
for i := uintptr(0); i < bucketCnt; i++ {
// 找是否有匹配的高8位
if b.tophash[i] != top {
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) //指針運算計算獲取key的位址
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if alg.equal(key, k) {
v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize)) //指針運算計算value的位址
if t.indirectvalue() {
v = *((*unsafe.Pointer)(v))
}
return v
}
}
}
return unsafe.Pointer(&zeroVal[0]) //永遠有一個zero object來return
}
怎麼寫
Go底層map存數據(map[k] = v)的方法主要是mapassign這個函數。和mapaccess類似,都要先算出hash值,然後找到對應的位置,mapassign的區別在於這個位置可能不存在及擴容問題。
func reflect_mapassign(t *maptype, h *hmap, key unsafe.Pointer, val unsafe.Pointer) {
p := mapassign(t, h, key)
typedmemmove(t.elem, p, val)
}
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
if h == nil {
panic(plainError("assignment to entry in nil map"))
}
if raceenabled {
callerpc := getcallerpc()
pc := funcPC(mapassign)
racewritepc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled {
msanread(key, t.key.size)
}
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 計算hash值
alg := t.key.alg
hash := alg.hash(key, uintptr(h.hash0))
h.flags ^= hashWriting
// 如果hmap的bucket爲空,則需要先創建一個新的bucket數組,底層用mallocgc實現。
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
bucket := hash & bucketMask(h.B)
// 如果hmap正在擴容,會把老數據遷移到新的位址去,具體見下面的map擴容。
if h.growing() {
growWork(t, h, bucket)
}
// 找到對應的bucket
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
top := tophash(hash) //計算高8位
var inserti *uint8
var insertk unsafe.Pointer
var val unsafe.Pointer
bucketloop:
for {
for i := uintptr(0); i < bucketCnt; i++ {
// 找到一個bucket中tophash[i]的值不爲top,並且這個tophash的值不爲空及inserti爲空的位址,然後賦值。
if b.tophash[i] != top {
if isEmpty(b.tophash[i]) && inserti == nil {
inserti = &b.tophash[i]
insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
}
// emptyRest表示,這個對應的bucket及其overflow bucket都沒有多餘的空間,則需要建新的cell或新的entry,跳到91行
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
if !alg.equal(key, k) {
continue
}
// 如果這個key已經存在就需要替換掉原來的值
if t.needkeyupdate() {
typedmemmove(t.key, k, key)
}
val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
goto done
}
ovf := b.overflow(t)
if ovf == nil {
break
}
b = ovf
}
// 如果達到擴容因子最大值,或者有太多的overflow bucket,並且hmap沒有在擴容狀態,則需對hmap擴容。
if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
hashGrow(t, h)
goto again // Growing the table invalidates everything, so try again
}
// 如果當前所有的bucket及其overlow bucket都滿了,就需要建一個新的overflow bucket
if inserti == nil {
newb := h.newoverflow(t, b)
inserti = &newb.tophash[0]
insertk = add(unsafe.Pointer(newb), dataOffset)
val = add(insertk, bucketCnt*uintptr(t.keysize))
}
if t.indirectkey() {
kmem := newobject(t.key)
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectvalue() {
vmem := newobject(t.elem)
*(*unsafe.Pointer)(val) = vmem
}
typedmemmove(t.key, insertk, key)
*inserti = top
h.count++
done:
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
h.flags &^= hashWriting
if t.indirectvalue() {
val = *((*unsafe.Pointer)(val))
}
return val
}
map擴容
Go中map bucket數量的初始化是根據hamp中的b,即擴容因子定義的,見/src/runtime/map.go中的mapBufferArray函數。
map擴容條件:
· bucket及其overflow bucket已經寫滿的時候可以擴容;
· 當元素> 6.5 * #bucket 的時候可擴容;
· 非擴容狀態下才可以擴容。
map擴容遷移數據的方式:
map擴容時,會創建一個爲原來bucket數組兩倍大的新bucket數組,寫入新數據,用一個指針來指向舊的bucket數組。Go map不會在擴容後就一下子把舊的數據移到新的bucket數組內,只有當1觸發,即訪問到舊數據的時候。然後執行2,通過指向舊數據的指針找到對應的舊數據,重新計算hash值後hash到新的bucket數組去。最後3,把指向舊bucket數組的指針釋放掉,等待gc。