Go 基礎 Map

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。

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章