Golang map 原始碼解析和結構圖解 https://www.weiboke.online

Wu_XMing發表於2019-01-25

歡迎訪問我的部落格網站,可聊天,可寫文章https://www.weiboke.online

簡單用例

func main() {
	a := make(map[int]int)
	a[2] = 1
	fmt.Println(a[2])
}
複製程式碼

golang 使用map是相當的簡單,直接使用,無需調包。出於興趣也是出於疑問,想深入瞭解map,以及map為啥不是併發安全。上面的用例很簡單,使用map一定要make一下,才能使用,否則會因為map為nil而panic。

map的容器

map的容器結構是由bmap結構陣列組成,只有理解了bmap的結構,才能理解map如何CURD操作

Golang map 原始碼解析和結構圖解 https://www.weiboke.online

如何make一個map

對於上面的例子裡,要make一個map會呼叫makemap_small函式,初始化一個hmap的結構

func makemap_small() *hmap {
	h := new(hmap)  //new 一個hmap
	h.hash0 = fastrand() //計算hash種子,用於hash時的引數 
	return h
}

// A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields
}
複製程式碼
  • count : map中key的個數
  • buckets:一個指標,指向bmap陣列的頭指標
  • B :buckets數量的log2

如何map[k]=v

型別的不同,可能會呼叫不同的函式,不過原理是類似的。對於上面的用例,會呼叫mapassign_fast64函式


func mapassign_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer {
	if h == nil {
		panic(plainError("assignment to entry in nil map"))
	}
    ...
	if h.flags&hashWriting != 0 {
		throw("concurrent map writes")
	}
	hash := t.key.alg.hash(noescape(unsafe.Pointer(&key)), uintptr(h.hash0))
	// Set hashWriting after calling alg.hash for consistency with mapassign.
	h.flags |= hashWriting

	if h.buckets == nil {
		h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
	}

again:
	bucket := hash & bucketMask(h.B)
	...
	b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))

	var insertb *bmap
	var inserti uintptr
	var insertk unsafe.Pointer

	for {
		for i := uintptr(0); i < bucketCnt; i++ {
			if b.tophash[i] == empty {
				if insertb == nil {
					insertb = b
					inserti = i
				}
				continue
			}
			k := *((*uint64)(add(unsafe.Pointer(b), dataOffset+i*8)))
			if k != key {
				continue
			}
			insertb = b
			inserti = i
			goto done
		}
		...
	}

    ...

	insertb.tophash[inserti&(bucketCnt-1)] = tophash(hash) // mask inserti to avoid bounds checks

	insertk = add(unsafe.Pointer(insertb), dataOffset+inserti*8)
	// store new key at insert position
	*(*uint64)(insertk) = key

	h.count++

done:
	val := add(unsafe.Pointer(insertb), dataOffset+bucketCnt*8+inserti*uintptr(t.valuesize))
	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	h.flags &^= hashWriting
	return val
}
複製程式碼

從上面的程式碼可以看到,如果map沒被初始化會panic,map會做簡單的併發檢查,即檢查寫標誌位(h.flags&hashWriting)是否被設定為1,當為1是則panic,但這種檢查並不有效,尤其在多核處理器上,所以map不是併發安全。接著計算出key的hash值,並把寫標誌位置為1。buckets是map的容器,用來裝值。如果buckets為空,初始化一個物件。bucketMask計算掩碼值(如果h.B=4,則bucketMask(h.B)=1111(2);15(10)),通過hash和掩碼值相與得到是第幾個bucket。根據bucket值,計算偏移獲取bmap指標值。開始遍歷查詢是否有空的位置,存放這個key,或者是否有相同的key。如果bmap裡面沒有相同的key並且也有位置存放這個key,然後將tophash[inserti&(bucketCnt-1)]賦值為hash值的第一個位元組,主要用來標誌已存放有值。計算存放key的位置指標,賦值為key,將hmap的count加一。最後計算存放val的位置指標,將寫標誌位置零,將指標返回,編譯器會自動新增指令,將使用者程式碼中的value存到這個指標中。如果key已存在,就會直接跳到done區,也就是計算val的位置指標,將寫標誌位置零,將指標返回。

跳過了map增長之後的程式碼細節,可以後面繼續深入。

如何獲取map[k]的value值

弄明白上面如何存放value的原理,取也就是相當的簡單了。

func mapaccess1_fast64(t *maptype, h *hmap, key uint64) unsafe.Pointer {
	...
	if h == nil || h.count == 0 {
		return unsafe.Pointer(&zeroVal[0])
	}
	if h.flags&hashWriting != 0 {
		throw("concurrent map read and map write")
	}
	var b *bmap
	if h.B == 0 {
		// One-bucket table. No need to hash.
		b = (*bmap)(h.buckets)
	} else {
		hash := t.key.alg.hash(noescape(unsafe.Pointer(&key)), uintptr(h.hash0))
		m := bucketMask(h.B)
		b = (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
		...
	}
	for ; b != nil; b = b.overflow(t) {
		for i, k := uintptr(0), b.keys(); i < bucketCnt; i, k = i+1, add(k, 8) {
			if *(*uint64)(k) == key && b.tophash[i] != empty {
				return add(unsafe.Pointer(b), dataOffset+bucketCnt*8+i*uintptr(t.valuesize))
			}
		}
	}
	return unsafe.Pointer(&zeroVal[0])
}

複製程式碼

獲取value值的過程略寫了,過程與存value值類似。首先找到bucket的位置,取得bmap的指標,然後開始遍歷查詢是否存在這個key,有就返回存放value的指標值,否則返回nil。

相關文章