Go源碼閱讀——map.go

【博文目錄>>>】 【項目地址>>>】


Go Map實現

map.go文件包含Go的映射類型的實現。

映射只是一個哈希表。數據被安排在一系列存儲桶中。每個存儲桶最多包含8個鍵/元素對。哈希的低位用於選擇存儲桶。每個存儲桶包含每個哈希的一些高階位,以區分單個存儲桶中的條目。

如果有8個以上的鍵散列到存儲桶中,則我們會鏈接到其他存儲桶。

當散列表增加時,我們將分配一個兩倍大數組作爲新的存儲桶。將存儲桶以增量方式從舊存儲桶陣列複製到新存儲桶陣列。

映射迭代器遍歷存儲桶數組,並按遍歷順序返回鍵(存儲桶#,然後是溢出鏈順序,然後是存儲桶索引)。爲了維持迭代語義,我們絕不會在鍵的存儲桶中移動鍵(如果這樣做,鍵可能會返回0或2次)。在擴展表時,迭代器將保持對舊錶的迭代,並且必須檢查新表是否將要迭代的存儲桶(“撤離”)到新表中。

選擇loadFactor:太大了,我們有很多溢出桶,太小了,我們浪費了很多空間。一些不同負載的統計信息:(64位,8字節密鑰和elems)

  loadFactor    %overflow  bytes/entry     hitprobe    missprobe
        4.00         2.13        20.77         3.00         4.00
        4.50         4.05        17.30         3.25         4.50
        5.00         6.85        14.77         3.50         5.00
        5.50        10.55        12.94         3.75         5.50
        6.00        15.27        11.67         4.00         6.00
        6.50        20.90        10.79         4.25         6.50
        7.00        27.14        10.15         4.50         7.00
        7.50        34.03         9.73         4.75         7.50
        8.00        41.10         9.40         5.00         8.00
%overflow = 具有溢出桶的桶的百分比
bytes/entry = 每個鍵值對使用的字節數
hitprobe = 查找存在的key時要檢查的條目數
missprobe = 查找不存在的key要檢查的條目數

數據結構

重要常量

const (
	// 桶可以容納的最大鍵/值對數量。
    bucketCntBits = 3
	bucketCnt     = 1 << bucketCntBits

	// 觸發增長的存儲桶的最大平均負載爲6.5。表示爲loadFactorNum/loadFactDen,以允許整數數學運算。
	loadFactorNum = 13
	loadFactorDen = 2

	// 保持內聯的最大鍵或elem大小(而不是每個元素的malloc分配)。
    // 必須適合uint8。
    // 快速版本不能處理大問題 - cmd/compile/internal/gc/walk.go中快速版本的臨界大小最多必須是這個元素。
	maxKeySize  = 128
	maxElemSize = 128

	// 數據偏移量應爲bmap結構的大小,但需要正確對齊。對於amd64p32,
	// 即使指針是32位,這也意味着64位對齊。
	dataOffset = unsafe.Offsetof(struct {
		b bmap
		v int64
	}{}.v)

	// 可能的tophash值。我們爲特殊標記保留一些可能性。
    // 每個存儲桶(包括其溢出存儲桶,如果有的話)在遷移狀態下將具有全部或沒有條目
    //(除了evacuate()方法期間,該方法僅在映射寫入期間發生,因此在此期間沒有其他人可以觀察該映射)。
    // 所以合法的 tophash(指計算出來的那種),最小也應該是4,小於4的表示的都是我們自己定義的狀態值
    
    // 此單元格是空的,並且不再有更高索引或溢出的非空單元格。
	emptyRest      = 0
	// 這個單元格是空的
	emptyOne       = 1 
	// 鍵/元素有效。條目已被遷移到大表的前半部分。
	evacuatedX     = 2 
	// 與上述相同,但遷移到大表的後半部分。
	evacuatedY     = 3
	// 單元格是空的,桶已已經被遷移。
	evacuatedEmpty = 4
	// 一個正常填充的單元格的最小tophash
	minTopHash     = 5

	// 標誌位
	iterator     = 1 // 可能有一個使用桶的迭代器
	oldIterator  = 2 // 可能有一個使用oldbuckets的迭代器
	hashWriting  = 4 // 一個goroutine正在寫映射
	sameSizeGrow = 8 // 當前的映射增長是到一個相同大小的新映射

	noCheck = 1<<(8*sys.PtrSize) - 1 // 用於迭代器檢查的哨兵桶ID
)

const maxZero = 1024 // 必須與cmd/compile/internal/gc/walk.go:zeroValSize中的值匹配
var zeroVal [maxZero]byte // 用於:1、指針空時,返回unsafe.Pointer;2、用於幫助判斷空指針;3、防止指針越界

存儲結構定義

hmap是go中map結構的定義,其內容如下

type hmap struct {
	// 注意:hmap的格式也編碼在cmd/compile/internal/gc/reflect.go中。確保這與編譯器的定義保持同步。
	// #存活元素==映射的大小。必須是第一個(內置len()使用)
	count     int 
	flags     uint8
	// 桶數的log_2(最多可容納loadFactor * 2 ^ B個元素,再多就要擴容)
	B         uint8
	// 溢出桶的大概數量;有關詳細信息,請參見incrnoverflow
	noverflow uint16
	// 哈希種子
	hash0     uint32 // hash seed

    // 2^B個桶的數組。如果count == 0,則可能爲nil。
	buckets    unsafe.Pointer
	// 上一存儲桶數組,只有當前桶的一半大小,只有在增長時才爲非nil
	oldbuckets unsafe.Pointer
	// 遷移進度計數器(小於此的桶表明已被遷移)
	nevacuate  uintptr

    // 可選擇字段,溢出桶的內容全部在這裏
	extra *mapextra
}

mapextra是ma的溢出數據的定義,內容如下:

/**
 * mapextra包含並非在所有map上都存在的字段。
 **/
type mapextra struct {
	// 如果key和elem都不包含指針並且是內聯的,則我們將存儲桶類型標記爲不包含指針。這樣可以避免掃描此類映射。
    // 但是,bmap.overflow是一個指針。爲了使溢出桶保持活動狀態,我們將指向所有溢出桶的指針存儲在hmap.extra.overflow
    // 和hmap.extra.oldoverflow中。僅當key和elem不包含指針時,才使用overflow和oldoverflow。
    // overflow包含hmap.buckets的溢出桶。 oldoverflow包含hmap.oldbuckets的溢出存儲桶。
    // 間接允許在Hiter中存儲指向切片的指針。
	overflow    *[]*bmap
	oldoverflow *[]*bmap

	// nextOverflow擁有一個指向空閒溢出桶的指針。
	nextOverflow *bmap
}

bmap是map的桶定義,其他內容如下

/**
 * go映射的桶結構
 **/
type bmap struct {
	// tophash通常包含此存儲桶中每個鍵的哈希值的最高字節。如果tophash[0] < minTopHash,
	// 隨後是bucketCnt鍵,再後是bucketCnt元素。
	tophash [bucketCnt]uint8
    // 注意:將所有鍵打包在一起,然後將所有elems打包在一起,使代碼比交替key/elem/key/elem/...複雜一些,
    // 但是它使我們可以省去填充,例如,映射[int64] int8。後跟一個溢出指針。
}

hiter是map的替代器定義,其他內容如下

/**
 * 哈希迭代結構。
 * 如果修改了hiter,還請更改cmd/compile/internal/gc/reflect.go來指示此結構的佈局。
 **/
type hiter struct {
    // 必須處於第一位置。寫nil表示迭代結束(請參閱cmd/internal/gc/range.go)。
	key         unsafe.Pointer
	// 必須位於第二位置(請參閱cmd/internal/gc/range.go)。
	elem        unsafe.Pointer
	t           *maptype // map類型
	h           *hmap
	// hash_iter初始化時的bucket指針
	buckets     unsafe.Pointer
	// 當前迭代的桶
	bptr        *bmap
	// 使hmap.buckets溢出桶保持活動狀態
	overflow    *[]*bmap
	// 使hmap.oldbuckets溢出桶保持活動狀態
	oldoverflow *[]*bmap
	// 存儲桶迭代始於指針位置
	startBucket uintptr        // bucket iteration started at
	// 從迭代期間開始的桶內距離start位置的偏移量(應該足夠大以容納bucketCnt-1)
	offset      uint8
	// 已經從存儲桶數組的末尾到開頭纏繞了,迭代標記,爲true說明迭代已經可以結束了
	wrapped     bool
	B           uint8 // 與hmap中的B對應
	i           uint8
	bucket      uintptr
	checkBucket uintptr
}

其他數據結構

map中還使用到maptype數據結構。可以說明可見:https://github.com/Wang-Jun-Chao/go-source-read/blob/master/reflect/type_go.md

map存儲結構示意圖

在這裏插入圖片描述

創建map

go map創建

make(map[k]v)(map[k]v, hint)

小map創建

/**
 * 當在編譯時已知hint最多爲bucketCnt並且需要在堆上分配映射時,
 * makemap_small實現了make(map[k]v)和make(map[k]v, hint)的Go映射創建。
 **/
func makemap_small() *hmap {
	h := new(hmap)
	h.hash0 = fastrand()
	return h
}

大map創建

/**
 * 創建hmap,主要是對hint參數進行判定,不超出int可以表示的值
 **/
func makemap64(t *maptype, hint int64, h *hmap) *hmap {
	if int64(int(hint)) != hint {
		hint = 0
	}
	return makemap(t, int(hint), h)
}
/**
 * makemap實現Go map創建,其實現方法是make(map[k]v)和make(map[k]v, hint)。
 * 如果編譯器認爲map和第一個 bucket 可以直接創建在棧上,h和bucket 可能都是非空
 * 如果h!= nil,則可以直接在h中創建map。
 * 如果h.buckets != nil,則指向的存儲桶可以用作第一個存儲桶。
 **/
func makemap(t *maptype, hint int, h *hmap) *hmap {
    // 計算所需要的內存空間,並且判斷是是否會有溢出
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
	if overflow || mem > maxAlloc { // 有溢出或者分配的內存大於最大分配內存
		hint = 0
	}

	// 初始化hmap
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand() // 設置隨機數

	// 找到用於保存請求的元素數的大小參數B。
    // 對於hint<0,由於hint < bucketCnt,overLoadFactor返回false。
	B := uint8(0)
	// 按照提供的元素個數,找一個可以放得下這麼多元素的 B 值
	for overLoadFactor(hint, B) {
		B++
	}
	h.B = B

	// 如果B == 0,則分配初始哈希表,則稍後(在mapassign中)延遲分配buckets字段。
	// 如果hint爲零,則此內存可能需要一段時間。
	// 因爲如果 hint 很大的話,對這部分內存歸零會花比較長時間
	if h.B != 0 {
		var nextOverflow *bmap
		// 創建數據桶和溢出桶
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
		if nextOverflow != nil { // 溢出桶不爲空就將溢出桶掛到附加數據上
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}
	}

	return h
}

實際選用哪個函數很複雜,涉及的判定變量有:

  • 1、hint值,以及hint最終類型:
  • 2、逃逸分析結果
  • 3、BUCKETSIZE=8

創建map選擇的map函數分析在代碼:/usr/local/go/src/cmd/compile/internal/gc/walk.go:1218中

case OMAKEMAP:
		t := n.Type
		hmapType := hmap(t)
		hint := n.Left

		// var h *hmap
		var h *Node
		if n.Esc == EscNone {
			// Allocate hmap on stack.

			// var hv hmap
			hv := temp(hmapType)
			zero := nod(OAS, hv, nil)
			zero = typecheck(zero, ctxStmt)
			init.Append(zero)
			// h = &hv
			h = nod(OADDR, hv, nil)

			// Allocate one bucket pointed to by hmap.buckets on stack if hint
			// is not larger than BUCKETSIZE. In case hint is larger than
			// BUCKETSIZE runtime.makemap will allocate the buckets on the heap.
			// Maximum key and elem size is 128 bytes, larger objects
			// are stored with an indirection. So max bucket size is 2048+eps.
			if !Isconst(hint, CTINT) ||
				hint.Val().U.(*Mpint).CmpInt64(BUCKETSIZE) <= 0 {
				// var bv bmap
				bv := temp(bmap(t))

				zero = nod(OAS, bv, nil)
				zero = typecheck(zero, ctxStmt)
				init.Append(zero)

				// b = &bv
				b := nod(OADDR, bv, nil)

				// h.buckets = b
				bsym := hmapType.Field(5).Sym // hmap.buckets see reflect.go:hmap
				na := nod(OAS, nodSym(ODOT, h, bsym), b)
				na = typecheck(na, ctxStmt)
				init.Append(na)
			}
		}

		if Isconst(hint, CTINT) && hint.Val().U.(*Mpint).CmpInt64(BUCKETSIZE) <= 0 {
			// Handling make(map[any]any) and
			// make(map[any]any, hint) where hint <= BUCKETSIZE
			// special allows for faster map initialization and
			// improves binary size by using calls with fewer arguments.
			// For hint <= BUCKETSIZE overLoadFactor(hint, 0) is false
			// and no buckets will be allocated by makemap. Therefore,
			// no buckets need to be allocated in this code path.
			if n.Esc == EscNone {
				// Only need to initialize h.hash0 since
				// hmap h has been allocated on the stack already.
				// h.hash0 = fastrand()
				rand := mkcall("fastrand", types.Types[TUINT32], init)
				hashsym := hmapType.Field(4).Sym // hmap.hash0 see reflect.go:hmap
				a := nod(OAS, nodSym(ODOT, h, hashsym), rand)
				a = typecheck(a, ctxStmt)
				a = walkexpr(a, init)
				init.Append(a)
				n = convnop(h, t)
			} else {
				// Call runtime.makehmap to allocate an
				// hmap on the heap and initialize hmap's hash0 field.
				fn := syslook("makemap_small")
				fn = substArgTypes(fn, t.Key(), t.Elem())
				n = mkcall1(fn, n.Type, init)
			}
		} else {
			if n.Esc != EscNone {
				h = nodnil()
			}
			// Map initialization with a variable or large hint is
			// more complicated. We therefore generate a call to
			// runtime.makemap to initialize hmap and allocate the
			// map buckets.

			// When hint fits into int, use makemap instead of
			// makemap64, which is faster and shorter on 32 bit platforms.
			fnname := "makemap64"
			argtype := types.Types[TINT64]

			// Type checking guarantees that TIDEAL hint is positive and fits in an int.
			// See checkmake call in TMAP case of OMAKE case in OpSwitch in typecheck1 function.
			// The case of hint overflow when converting TUINT or TUINTPTR to TINT
			// will be handled by the negative range checks in makemap during runtime.
			if hint.Type.IsKind(TIDEAL) || maxintval[hint.Type.Etype].Cmp(maxintval[TUINT]) <= 0 {
				fnname = "makemap"
				argtype = types.Types[TINT]
			}

			fn := syslook(fnname)
			fn = substArgTypes(fn, hmapType, t.Key(), t.Elem())
			n = mkcall1(fn, n.Type, init, typename(n.Type), conv(hint, argtype), h)
		}

訪問map元素

go中訪問map元素是通過map[key]的方式進行,真正的元素訪問在go語言中有如下幾個方法

  • func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {…}:mapaccess1返回指向h[key]的指針。從不返回nil,如果鍵不在映射中,它將返回對elem類型的零對象的引用。對應go寫法:v := m[k]
  • func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {…}:方法同mapaccess1,僅多返回一個值用於表示是否找到對應元素。對應go寫法:v, ok := m[k]
  • func mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer) {…}:返回key和elem。由map迭代器使用,與mapaccess1相類似,只多返回了一個key。。對應go寫法:k,v := range m[k]。
  • func mapaccess1_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) unsafe.Pointer {…}:mapaccess1的包裝方法,獲取map中key對應的值,如果沒有找到就返回zero。對應go寫法:v := m[k]
  • func mapaccess2_fat(t *maptype, h *hmap, key, zero unsafe.Pointer) (unsafe.Pointer, bool) {…}:mapaccess2的包裝方法,獲取map中key對應的值,如果沒有找到就返回zero,並返回是否找到標記。。對應go寫法:v, ok := m[k]

其中mapaccess1,mapaccess2,mapaccessK方法大同小異,我們選擇mapaccesssK進行分析:

/**
 * 返回key和elem。由map迭代器使用,與mapaccess1相類似,只多返回了一個key
 * @param
 * @return
 **/
func mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer) {
	if h == nil || h.count == 0 { // map 爲空,或者元素數爲 0,直接返回未找到
		return nil, nil
	}
	hash := t.hasher(key, uintptr(h.hash0)) // 計算hash值
	// 計算掩碼:(1<<h.B)- 1,B=3,m=111;B=4,m=1111
	m := bucketMask(h.B)
	// 計算桶數
	// unsafe.Pointer(uintptr(h.buckets):基址
	// (hash&m)*uintptr(t.bucketsize)):偏移量,(hash&m)就是桶數
	b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))
	// h.oldbuckets不爲空,說明正在擴容,新的 buckets 裏可能還沒有老的內容
    // 所以一定要在老的桶裏面找,否則有可能可能找不到
	if c := h.oldbuckets; c != nil {
		if !h.sameSizeGrow() {
			// 如果不是同大小增長,那麼現在的老桶,只有新桶的一半,對應的mask也林減少一位
			m >>= 1
		}
		// 計算老桶的位置
		oldb := (*bmap)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize)))
		if !evacuated(oldb) { // 如果沒有遷移完,需要從老桶中找
			b = oldb
		}
	}
	// tophash 取其高 8bit 的值
	top := tophash(hash)
bucketloop:
	for ; b != nil; b = b.overflow(t) {
	    // 一個 bucket 在存儲滿8個元素後,就再也放不下了
        // 這時候會創建新的 bucket掛在原來的bucket的overflow指針成員上
		for i := uintptr(0); i < bucketCnt; i++ {
		    // 循環對比 bucket 中的 tophash 數組,
		    // 如果找到了相等的 tophash,那說明就是這個 bucket 了
			if b.tophash[i] != top {
			    // 如果找到值爲emptyRest,說明桶後面是空的,沒有值了,
			    // 無法找到對應的元素,,跳出bucketloop
				if b.tophash[i] == emptyRest {
					break bucketloop
				}
				continue
			}
			// 到這裏說明找到對應的hash值,具體是否相等還要判斷對應equal方法
			// 取k元素
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			if t.indirectkey() {
				k = *((*unsafe.Pointer)(k))
			}
			if t.key.equal(key, k) { // 如果爲值,說明真正找到了對應的元素
				e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
				if t.indirectelem() {
					e = *((*unsafe.Pointer)(e))
				}
				return k, e
			}
		}
	}
	return nil, nil
}

元素訪問示意圖
在這裏插入圖片描述

map元素賦值

map元素的賦值都通過方法mapassign進行

/**
 * 與mapaccess類似,但是如果map中不存在key,則爲該key分配一個位置。
 * @param
 * @return key對應elem的插入位置指針
 **/
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	if h == nil { // mil map不可以進行賦值
		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 := t.hasher(key, uintptr(h.hash0)) // 計算hash值

	// 在調用t.hasher之後設置hashWriting,因爲t.hasher可能會出現panic情況,在這種情況下,我們實際上並未執行寫入操作。
	h.flags ^= hashWriting

	if h.buckets == nil { // 如果桶爲空,就創建大小爲1的桶
		h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
	}

again:
    // 計算桶的位置,實際代表第幾個桶,(1<<h.B)-1
	bucket := hash & bucketMask(h.B)
	if h.growing() { // 是否在擴容
		growWork(t, h, bucket) // 進行擴容處理
	}
	// 計算桶的位置,指針地址
	b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
	// 計算高8位hash
	top := tophash(hash)

	var inserti *uint8 // 記錄
	var insertk unsafe.Pointer
	var elem unsafe.Pointer
bucketloop:
	for {
		for i := uintptr(0); i < bucketCnt; i++ { // 遍歷對應桶中的元素
			if b.tophash[i] != top {
			    // 在 b.tophash[i] != top 的情況下
                // 理論上有可能會是一個空槽位
                // 一般情況下 map 的槽位分佈是這樣的,e 表示 empty:
                // [h1][h2][h3][h4][h5][e][e][e]
                // 但在執行過 delete 操作時,可能會變成這樣:
                // [h1][h2][e][e][h5][e][e][e]
                // 所以如果再插入的話,會盡量往前面的位置插
                // [h1][h2][e][e][h5][e][e][e]
                //          ^
                //          ^
                //       這個位置
                // 所以在循環的時候還要順便把前面的空位置先記下來
				if isEmpty(b.tophash[i]) && inserti == nil {
					inserti = &b.tophash[i]
					insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
					elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
				}
				if b.tophash[i] == emptyRest { // i及之後的槽位都爲空,不需要再進行處理了
					break bucketloop
				}
				continue
			}
			// 已經找到一個i使得b.tophash[i] == top
			// 找到對應的k
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			if t.indirectkey() {
				k = *((*unsafe.Pointer)(k))
			}
			// 已經存儲的key和要傳入的key不相等,說明發生了hash碰撞
			if !t.key.equal(key, k) {
				continue
			}
			// 已經有一個key映射。更新它。
			if t.needkeyupdate() {
				typedmemmove(t.key, k, key)
			}
			elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
			goto done
		}
		// bucket的8個槽沒有滿足條件的能插入或者能更新的,去overflow裏繼續找
		ovf := b.overflow(t)
		// 如果overflow爲 nil,說明到了overflow鏈表的末端了
		if ovf == nil {
			break
		}
		// 賦值爲鏈表的下一個元素,繼續循環
		b = ovf
	}

	// 找不到鍵的映射。分配新單元格並添加條目。

	// 如果我們達到最大負載因子,或者我們有太多的溢出桶,而我們還沒有處於增長過程,那就開始增長。
	if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
	    // hashGrow的時候會把當前的bucket放到oldbucket裏
        // 但還沒有開始分配新的bucket,所以需要到again重試一次
        // 重試的時候在growWork裏會把這個key的bucket優先分配好
		hashGrow(t, h)
		goto again // Growing the table invalidates everything, so try again // 擴容表格會使所有內容失效,因此請重試
	}

	if inserti == nil {
		// 前面在桶裏找的時候,沒有找到能塞這個 tophash 的位置
        // 說明當前所有 buckets 都是滿的,分配一個新的 bucket
		newb := h.newoverflow(t, b)
		inserti = &newb.tophash[0]
		insertk = add(unsafe.Pointer(newb), dataOffset)
		elem = add(insertk, bucketCnt*uintptr(t.keysize))
	}

	// 在插入位置存儲新的key/elem
	if t.indirectkey() { // 插入key
		kmem := newobject(t.key)
		*(*unsafe.Pointer)(insertk) = kmem
		insertk = kmem
	}
	if t.indirectelem() { // 插入elem
		vmem := newobject(t.elem)
		*(*unsafe.Pointer)(elem) = 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.indirectelem() {
		elem = *((*unsafe.Pointer)(elem))
	}
	return elem
}

mapassign沒有對value進行操作,只是返回了需要value的地址信息,到底在哪裏進行了操作,我們以下面的程序爲例進行說明:map_go_summary.go

package main

import "fmt"

func main() {
	type P struct {
		Age [16]int
	}

	var a = make(map[P]int, 17)

	a[P{}] = 9999999

	for i := 0; i < 16; i++ {
		p := P{}
		p.Age[0] = i
		a[p] = i
	}
	fmt.Println(a)
}

運行: go tool compile -N -l -S map_go_summary.go獲得反彙編代碼,查看:第12行所做的操作

0x0061 00097 (map_go_summary.go:12)     PCDATA  $0, $2
0x0061 00097 (map_go_summary.go:12)     LEAQ    ""..autotmp_4+184(SP), DI
0x0069 00105 (map_go_summary.go:12)     XORPS   X0, X0
0x006c 00108 (map_go_summary.go:12)     PCDATA  $0, $0
0x006c 00108 (map_go_summary.go:12)     DUFFZERO        $266
0x007f 00127 (map_go_summary.go:12)     PCDATA  $0, $1
0x007f 00127 (map_go_summary.go:12)     LEAQ    type.map["".1]int(SB), AX
0x0086 00134 (map_go_summary.go:12)     PCDATA  $0, $0
0x0086 00134 (map_go_summary.go:12)     MOVQ    AX, (SP)
0x008a 00138 (map_go_summary.go:12)     PCDATA  $0, $1
0x008a 00138 (map_go_summary.go:12)     MOVQ    "".a+312(SP), AX
0x0092 00146 (map_go_summary.go:12)     PCDATA  $0, $0
0x0092 00146 (map_go_summary.go:12)     MOVQ    AX, 8(SP)
0x0097 00151 (map_go_summary.go:12)     PCDATA  $0, $1
0x0097 00151 (map_go_summary.go:12)     LEAQ    ""..autotmp_4+184(SP), AX
0x009f 00159 (map_go_summary.go:12)     PCDATA  $0, $0
0x009f 00159 (map_go_summary.go:12)     MOVQ    AX, 16(SP)
0x00a4 00164 (map_go_summary.go:12)     CALL    runtime.mapassign(SB) # 調用mapasssgin方法
0x00a9 00169 (map_go_summary.go:12)     PCDATA  $0, $1
0x00a9 00169 (map_go_summary.go:12)     MOVQ    24(SP), AX
0x00ae 00174 (map_go_summary.go:12)     MOVQ    AX, ""..autotmp_7+328(SP)
0x00b6 00182 (map_go_summary.go:12)     TESTB   AL, (AX)
0x00b8 00184 (map_go_summary.go:12)     PCDATA  $0, $0
0x00b8 00184 (map_go_summary.go:12)     MOVQ    $9999999, (AX) # 進行賦值操作

賦值的最後一步實際上是編譯器額外生成的彙編指令來完成的。

map刪除key

go中刪除map語句:delete(m, k),底層實現在是通過mapdelete進行

/**
 * 刪除key
 **/
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := funcPC(mapdelete)
		racewritepc(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.hasher(key, 0) // see issue 23734
		}
		return
	}
	if h.flags&hashWriting != 0 { // 當前map正在被寫,不能再寫
		throw("concurrent map writes")
	}

	hash := t.hasher(key, uintptr(h.hash0))

	// 在調用t.hasher之後設置hashWriting,因爲t.hasher可能會出現panic情況,
	// 在這種情況下,我們實際上並未執行寫入(刪除)操作。
	h.flags ^= hashWriting

	bucket := hash & bucketMask(h.B)
	if h.growing() {
		growWork(t, h, bucket)
	}
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
	bOrig := b
	top := tophash(hash)
search:
	for ; b != nil; b = b.overflow(t) {
		for i := uintptr(0); i < bucketCnt; i++ {
			if b.tophash[i] != top {
				if b.tophash[i] == emptyRest {
					break search
				}
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
			k2 := k
			if t.indirectkey() {
				k2 = *((*unsafe.Pointer)(k2))
			}
			if !t.key.equal(key, k2) { // 兩個key不相等
				continue
			}
			// 如果是間接指針,則僅清除鍵。
			if t.indirectkey() {
				*(*unsafe.Pointer)(k) = nil
			} else if t.key.ptrdata != 0 { 
				memclrHasPointers(k, t.key.size) // 清除內存數據
			}
			e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
			if t.indirectelem() { // elem是間接指針,將指針賦空
				*(*unsafe.Pointer)(e) = nil
			} else if t.elem.ptrdata != 0 { // 元素有指針數據,將清除指針數據
				memclrHasPointers(e, t.elem.size)
			} else { // 清除e的數據
				memclrNoHeapPointers(e, t.elem.size)
			}
			b.tophash[i] = emptyOne // 標記桶爲空
			// 如果存儲桶現在以一堆emptyOne狀態結束,則將其更改爲emptyRest狀態。
			// 將此功能設爲單獨的函數會很好,但是for循環當前不可內聯。
			if i == bucketCnt-1 {
			    // 有下一個溢出桶,並且溢出桶有值
				if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
					goto notLast
				}
			} else { // 沒有溢出桶了,但是當前位置之後的位置還有值
				if b.tophash[i+1] != emptyRest {
					goto notLast
				}
			}
			for { // 當前桶的當前位置之後都沒有值了
				b.tophash[i] = emptyRest // 標記當前位置已經空
				if i == 0 {
					if b == bOrig {
					    // 從初始存儲桶開始,我們已經處理完了。
						break 
					}
					// 查找上一個存儲桶,直到最後一個。
					c := b
					for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
					}
					i = bucketCnt - 1
				} else {
					i--
				}
				if b.tophash[i] != emptyOne {
					break
				}
			}
		notLast:
		    // hmap 的大小計數 -1
			h.count--
			break search
		}
	}

	if h.flags&hashWriting == 0 {
		throw("concurrent map writes")
	}
	h.flags &^= hashWriting
}

map擴容

在mapassign方法中我們看到了擴容的條件

  • 1、!h.growing() && (overLoadFactor(h.count+1, h.B):當前map未進行擴容,但是添加一個元素後,超過負載因子。負載因子是6.5,即:元素個數>=桶個數*6.5,需要進行擴容
  • 2、tooManyOverflowBuckets(h.noverflow, h.B):我們有太多的溢出桶。什麼情況下是溢出桶過多:
    • (1)當bucket總數 < 2^15 時,如果overflow的bucket總數 >= bucket的總數,那麼我們認爲overflow的桶太多了。
    • (2)當bucket總數 >= 215時,那我們直接和215比較,overflow的bucket >= 2^15時,即認爲溢出桶太多了。

兩種情況官方採用了不同的解決方法:

  • 針對(1),將B+1,進而hmap的bucket數組擴容一倍;
  • 針對(2),通過移動bucket內容,使其傾向於緊密排列從而提高bucket利用率。

如果map中有大量的哈希衝突,也會導致落入(2)中的條件,此時對bucket的內容進行移動其實沒什麼意義,反而會影響性能,所以理論上存在對Go map進行hash碰撞攻擊的可能性。

/**
 * map擴容
 **/
func hashGrow(t *maptype, h *hmap) {
	// 如果我們達到了負載因子,請擴容。
	// 否則,溢出桶過多,因此保持相同數量的桶並橫向“增長”。
	bigger := uint8(1)
	if !overLoadFactor(h.count+1, h.B) { // 增加一個元素,沒有超過負載因子
		bigger = 0
		h.flags |= sameSizeGrow
	}
	oldbuckets := h.buckets
	// 創建新桶
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

	flags := h.flags &^ (iterator | oldIterator)
	if h.flags&iterator != 0 {
		flags |= oldIterator
	}
	// 提交擴容(atomic wrt gc)
	h.B += bigger
	h.flags = flags
	h.oldbuckets = oldbuckets
	h.buckets = newbuckets
	h.nevacuate = 0
	h.noverflow = 0

	if h.extra != nil && h.extra.overflow != nil {
		// 將當前的溢出桶提升到老一代。
		if h.extra.oldoverflow != nil {
			throw("oldoverflow is not nil")
		}
		h.extra.oldoverflow = h.extra.overflow
		h.extra.overflow = nil
	}
	if nextOverflow != nil {
		if h.extra == nil {
			h.extra = new(mapextra)
		}
		h.extra.nextOverflow = nextOverflow
	}

	// 哈希表數據的實際複製是通過growWork()和evacuate()增量完成的。
}

/**
 * makeBucketArray爲map數據桶初始化底層數組。
 * 1<<b 是要分配的最小存儲桶數。
 * dirtyalloc應該爲nil或由makeBucketArray先前使用相同的t和b參數分配的bucket數組。
 * 如果dirtyalloc爲nil,則將分配一個新的後備數組,否則,dirtyalloc將被清除並重新用作後備數組。
 * @param
 * @return
 **/
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
	base := bucketShift(b)
	nbuckets := base
	// 對於小b,溢出桶不太可能出現。避免計算的開銷。
	if b >= 4 {
		// 加上所需的溢流桶的估計數量,以插入使用此值b的元素的中位數。
		nbuckets += bucketShift(b - 4)
		sz := t.bucket.size * nbuckets
		up := roundupsize(sz) // 計算mallocgc分配的內存
		if up != sz {
			nbuckets = up / t.bucket.size // 計算每個桶的內存大小
		}
	}

	if dirtyalloc == nil {
	    // 直接創建數組
		buckets = newarray(t.bucket, int(nbuckets))
	} else {
		// dirtyalloc先前是由上述newarray(t.bucket, int(nbuckets))生成的,但可能不爲空。
		buckets = dirtyalloc
		size := t.bucket.size * nbuckets
		// 進行內存清零
		if t.bucket.ptrdata != 0 {
			memclrHasPointers(buckets, size)
		} else {
			memclrNoHeapPointers(buckets, size)
		}
	}

    // 實際計算的桶數據和最初的桶數不一樣
	if base != nbuckets {
		// 我們預先分配了一些溢出桶。
        // 爲了使跟蹤這些溢出桶的開銷降到最低,我們使用以下約定:如果預分配的溢出桶的溢出指針爲nil,則通過碰撞指針還有更多可用空間。
        // 對於最後一個溢出存儲區,我們需要一個安全的非nil指針;使用buckets。
		nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize))) // 計算下一個溢出桶
		last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize))) // 計算最後一個溢出桶
		last.setoverflow(t, (*bmap)(buckets))
	}
	return buckets, nextOverflow
}

/**
 * 桶增長
 **/
func growWork(t *maptype, h *hmap, bucket uintptr) {
	// 確保我們遷移將要使用的存儲桶對應的舊存儲桶
	evacuate(t, h, bucket&h.oldbucketmask())

    // 遷移一箇舊桶,會使過程標記在growing
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}

/**
 * 進行遷移
 * @param
 * @param
 * @param oldbucket 需要遷移的桶
 * @return
 **/
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
	b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
	newbit := h.noldbuckets() // 值形如111...111
	if !evacuated(b) {
		// TODO:如果沒有迭代器使用舊的存儲桶,則重用溢出存儲桶而不是使用新的存儲桶。(如果爲!oldIterator。)

		// xy包含x和y(低和高)遷移目的地。
        // x 表示新 bucket 數組的前(low)半部分
        // y 表示新 bucket 數組的後(high)半部分
		var xy [2]evacDst
		x := &xy[0]
		x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
		x.k = add(unsafe.Pointer(x.b), dataOffset)
		x.e = add(x.k, bucketCnt*uintptr(t.keysize))

		if !h.sameSizeGrow() { // 非同大小增長
			// 僅當我們變得更大時才計算y指針。否則GC可能會看到錯誤的指針。
			y := &xy[1]
			y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
			y.k = add(unsafe.Pointer(y.b), dataOffset)
			y.e = add(y.k, bucketCnt*uintptr(t.keysize))
		}

		for ; b != nil; b = b.overflow(t) {
			k := add(unsafe.Pointer(b), dataOffset)
			e := add(k, bucketCnt*uintptr(t.keysize))
			for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
				top := b.tophash[i]
				if isEmpty(top) {
					b.tophash[i] = evacuatedEmpty
					continue
				}
				if top < minTopHash {
					throw("bad map state")
				}
				k2 := k
				if t.indirectkey() {
					k2 = *((*unsafe.Pointer)(k2))
				}
				var useY uint8
				if !h.sameSizeGrow() { // 擴容一倍
					// 計算散列值以做出遷移決定(是否需要將此key/elem發送到存儲桶x或存儲桶y)。
					hash := t.hasher(k2, uintptr(h.hash0))
					if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
					    // 對於一般情況,key必須是自反的,即 key==key,但是對於特殊情況,比如浮點值n1、n2(都是NaN),n1==n2是不成立的,對於這部分key,我們使用最低位進行隨機選擇,讓它們到Y部分
						// 如果key != key(NaNs),則哈希可能(可能會)與舊哈希完全不同。而且,它是不可重現的。
						// 在存在迭代器的情況下,要求具有可重複性,因爲我們的遷移決策必須與迭代器所做的任何決策相匹配。
						// 幸運的是,我們可以自由發送這些key。同樣,tophash對於這些key也沒有意義。
						// 我們讓低位的hophash決定遷移。我們爲下一個級別重新計算了一個新的隨機tophash,以便在多次增長之後,
						// 這些key將在所有存儲桶中平均分配
						useY = top & 1
						top = tophash(hash)
					} else {
					    // 假設newbit有6位,則newbit=111111
					    // 如果hash的低6位不0則元素必須去高位
						if hash&newbit != 0 {
							useY = 1
						}
					}
				}

				if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
					throw("bad evacuatedN")
				}

				b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
				dst := &xy[useY] // 遷移目的地

				if dst.i == bucketCnt {
					dst.b = h.newoverflow(t, dst.b)
					dst.i = 0
					dst.k = add(unsafe.Pointer(dst.b), dataOffset)
					dst.e = add(dst.k, bucketCnt*uintptr(t.keysize))
				}
				// 掩碼dst.i作爲優化,以避免邊界檢查
				dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
				if t.indirectkey() {
					*(*unsafe.Pointer)(dst.k) = k2 // copy pointer // 拷貝指針
				} else {
					typedmemmove(t.key, dst.k, k) // copy elem // 拷貝元素
				}
				if t.indirectelem() {
					*(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
				} else {
					typedmemmove(t.elem, dst.e, e)
				}
				dst.i++
				// These updates might push these pointers past the end of the
				// key or elem arrays.  That's ok, as we have the overflow pointer
				// at the end of the bucket to protect against pointing past the
				// end of the bucket.
				// 這些更新可能會將這些指針推到key或elem數組的末尾。
				// 沒關係,因爲我們在存儲桶的末尾有溢出指針,以防止指向存儲桶的末尾。
				dst.k = add(dst.k, uintptr(t.keysize))
				dst.e = add(dst.e, uintptr(t.elemsize))
			}
		}
		// 取消鏈接溢出桶並清除key/elem,以幫助GC。
		if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
			b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
			// 因爲遷移狀態一直保持在那裏,所以要保留b.tophash。
			ptr := add(b, dataOffset)
			n := uintptr(t.bucketsize) - dataOffset
			memclrHasPointers(ptr, n)
		}
	}

	if oldbucket == h.nevacuate {
		advanceEvacuationMark(h, t, newbit)
	}
}

func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
	h.nevacuate++
	// 實驗表明,1024的殺傷力至少高出一個數量級。無論如何都要將其放在其中以確保O(1)行爲。
	stop := h.nevacuate + 1024
	if stop > newbit {
		stop = newbit
	}
	// 遷移直到不成功或者等於stop
	for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
		h.nevacuate++
	}
	if h.nevacuate == newbit { // newbit == # of oldbuckets
		// 增長已經完成。自由使用舊的主存儲桶數組。
		h.oldbuckets = nil
		// 也可以丟棄舊的溢出桶。
        // 如果迭代器仍在引用它們,則迭代器將保留指向切片的指針。
		if h.extra != nil {
			h.extra.oldoverflow = nil
		}
		h.flags &^= sameSizeGrow
	}
}

biggerSizeGrow示意圖:桶數組增大後,原來同一個桶的數據可以被分別移動到上半區和下半區。
在這裏插入圖片描述

sameSizeGrow示意圖:sameSizeGrow 之後,數據排列更緊湊。
在這裏插入圖片描述

indirectkey和indirectvalue

indirectkey和indirectvalue在代碼中經常出現,他們代表的是什麼呢?indirectkey和indirectvalue在map裏實際存儲的是key和elem的指針。使用指針,在GC掃描時,會進行二次掃描操作,找出指針所代表的對象,所以掃描的對象更多。key/elem是indirect還是indirect是由編譯器來決定的,依據是:

  • key > 128 字節時,indirectkey = true
  • value > 128 字節時,indirectvalue = true

下面使了兩用用例來測試

package main

import "fmt"

func main() {
	type P struct { // int在我的電腦上是8字節
		Age [16]int 
	}

	var a = make(map[P]int, 16)

	for i := 0; i < 16; i++ {
		p := P{}
		p.Age[0] = i
		a[p] = i
	}
	fmt.Println(a)
}

maptype.flags各個位表示的含義:

  • 0b00000001: indirectkey,間接key
  • 0b00000010: indirectelem,間接elem
  • 0b00000100: reflexivekey,key是自反的,即:key==key總是爲true,
  • 0b00001000: needkeyupdate,需要更新key
  • 0b00010000: hashMightPanic,key的hash函數可能有panic
    調式時可以看到
    t的flags值:4,說明是非indirectkey

在這裏插入圖片描述

package main

import "fmt"

func main() {
	type P struct {
		Age [16]int
		
	}

	var a = make(map[P]int, 16)

	for i := 0; i < 16; i++ {
		p := P{}
		p.Age[0] = i
		a[p] = i
	}
	fmt.Println(a)
}

調式時可以看到
t的flags值:5,說明是indirectkey
在這裏插入圖片描述

overflow

overflow出現的場景:當有多個不同的key都hash到同一個桶的時候,桶的8個位置不夠用,此時就會overflow。

獲取overflow的方式,從 h.extra.nextOverflow中拿overflow桶,如果拿到,就放進 hmap.extra.overflow 數組,並讓b的overflow指針指向這個桶。如果沒找到,那就new一個新的桶。並且讓b的overflow指針指向這個新桶,同時將新桶添加到h.extra.overflow數組中

/**
 * 創建新的溢出桶
 * @param
 * @return 新的溢出桶指針
 **/
func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {
	var ovf *bmap
	// 已經有額外數據,並且額外數據的nextOverflow不爲空,
	if h.extra != nil && h.extra.nextOverflow != nil {
		// 我們有預分配的溢出桶可用。有關更多詳細信息,請參見makeBucketArray。
		ovf = h.extra.nextOverflow
		if ovf.overflow(t) == nil {
			// 我們不在預分配的溢出桶的盡頭。撞到指針。
			h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))
		} else {
			// 這是最後一個預分配的溢出存儲桶。重置此存儲桶上的溢出指針,該指針已設置爲非nil標記值,現在要設置成nil
			ovf.setoverflow(t, nil)
			h.extra.nextOverflow = nil
		}
	} else {
	    // 沒有額外數據,創建新的溢出桶
		ovf = (*bmap)(newobject(t.bucket))
	}
	// 增加溢出桶計數
	h.incrnoverflow()
	if t.bucket.ptrdata == 0 { // 如果沒有指針數據
		h.createOverflow() // 創建額外的溢出數據
		*h.extra.overflow = append(*h.extra.overflow, ovf) // 將溢出桶添加到溢出數組中
	}
	b.setoverflow(t, ovf)
	return ovf
}
/**
 * 創建h的溢出桶
 * @param
 * @return
 **/
func (h *hmap) createOverflow() {
	if h.extra == nil {
		h.extra = new(mapextra)
	}
	if h.extra.overflow == nil {
		h.extra.overflow = new([]*bmap)
	}
}
/**
 * incrnoverflow遞增h.noverflow。
 * noverflow計算溢出桶的數量。
 * 這用於觸發相同大小的map增長。
 * 另請參見tooManyOverflowBuckets。
 * 爲了使hmap保持較小,noverflow是一個uint16。
 * 當存儲桶很少時,noverflow是一個精確的計數。
 * 如果有很多存儲桶,則noverflow是一個近似計數。
 * @param
 * @return
 **/
func (h *hmap) incrnoverflow() {
	// 如果溢出存儲桶的數量與存儲桶的數量相同,則會觸發相同尺寸的map增長。
    // 我們需要能夠計數到1<<h.B。
	if h.B < 16 { // 說是map中的元素比較少,少於(2^h.B)個
		h.noverflow++
		return
	}
	// 以概率1/(1<<(h.B-15))遞增。
    //當我們達到1 << 15-1時,我們將擁有大約與桶一樣多的溢出桶。
	mask := uint32(1)<<(h.B-15) - 1
	// 例如:如果h.B == 18,則mask == 7,fastrand&7 == 0,概率爲1/8。
	if fastrand()&mask == 0 {
		h.noverflow++
	}
}

map方法的變種

mapaccess1、 mapaccess2、mapassign和mapdelete都有32位、64位和string 類型的變種,對對應的文件位置:

  • $GOROOT/src/runtime/map_fast32.go
  • $GOROOT/src/runtime/map_fast64.go
  • $GOROOT/src/runtime/map_faststr.go

go map設計優缺點

go的map設計貼近底層,充分利用了內存佈局。一般情況下元素的元素訪問非常快。不足:go中的map使用拉鍊法解決hash衝突,當元素hash衝突比較多的時候會,需要經常擴容。map本身提供的方法比較少,不如其語言如java,c#豐富。

完整源碼

https://github.com/Wang-Jun-Chao/go-source-read/blob/master/runtime/map_go.md

參考文檔

https://github.com/cch123/golang-notes/blob/master/map.md
http://yangxikun.github.io/golang/2019/10/07/golang-map.html

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