go语言中map结构源码分析

go语言中的map结构

Go语言中的map是一种无序的数据结构,它存储了键值对(key-value pairs)。每个键都与一个唯一的值相关联,可以通过键来访问和修改对应的值。
Map的声明方式为:var map_name map[key_type]value_type。其中,key_type表示键的类型,value_type表示值的类型。例如,声明一个整型的键值对映射可以写成:var myMap map[int]string。
创建一个新的空Map可以使用make函数,例如:myMap := make(map[int]string)。也可以直接声明并初始化一个非空的Map,例如:myMap := map[int]string{1: “one”, 2: “two”}。
Map中的元素是无序的,因此不能通过索引来访问元素。要访问一个键对应的值,可以使用以下语法:value, ok := myMap[key]。其中,value表示获取到的值,ok是一个布尔类型的标志位,表示该键是否存在于Map中。如果键不存在,ok的值为false。

下面,我们结合源码,来分析一波。万字文章哦。
喜欢的收藏点赞加关注哦。

一、map结构体

// A header for a Go map.
type hmap struct {
	// Note: hmap的格式也编码在cmd/compile/internal/reflectdata/reflect.go中。
	// 请确保这与编译器的定义保持同步。
	count     int // # 元素数量。必须是第一个(由len()内置)。可以由 len(map) 函数获取
	flags     uint8		//标志位 iterator oldIterator hashWriting sameSizeGrow
	B         uint8  // 普通桶的数量(1<<B)的log_2。比如:B为1,那么桶的数量为2,B=log2(桶数量)
	noverflow uint16 // 溢出桶的大致数量;详见 incrnoverflow  (大致数量是因为:可能创建溢出桶,也可能不创建溢出桶)
	hash0     uint32 // hash seed,类似盐值,计算key的hash值时传递的参数

	buckets    unsafe.Pointer // 普通桶的地址。如果count==0,则可能为nil。
	oldbuckets unsafe.Pointer // 旧桶的数量。前一个大小为一半的bucket数组,仅在增长时为非nil:仅在扩容期间非空:非等量扩容期间为 buckets 的一半大小,等量扩容与 buckets 等大
	nevacuate  uintptr        // 迁移进度计数器(已疏散少于此数量的桶)迁移的桶数量,只有第0个桶迁移之后,才会更新此字段

	extra *mapextra // 可选字段:应付GC回收的字段。用于防止 k-v 不是指针的情况下,溢出桶 被 GC
}
// A bucket for a Go map.
type bmap struct {
	// tophash generally contains the top byte of the hash value  // tophash通常包含该bucket中每个键的哈希值的顶部字节(最高8位,刚好一个字节)。
	// for each key in this bucket. If tophash[0] < minTopHash,   // 如果tophash[0]<minTopHash,
	// tophash[0] is a bucket evacuation state instead.           // 则tophash[0]为bucket已迁移状态。
	tophash [bucketCnt]uint8       // keys [bucketCnt]keytype;values  [bucketCnt]valuetype;padding  uintptr;overflow uintptr
	// Followed by bucketCnt keys and then bucketCnt elems.      // 然后是bucketCnt键,然后是buketCnt elems。
	// NOTE: packing all the keys together and then all the elems together makes the   // 将所有密钥打包在一起,然后将所有elem打包在一起
	// code a bit more complicated than alternating key/elem/key/elem/... but it allows // 会使代码比交替使用key/elem/key/elem/更复杂。。。
	// us to eliminate padding which would be needed for, e.g., map[int64]int8.        // 但它允许我们消除例如map[int64]int8所需的填充。
	// Followed by an overflow pointer.							// 后面跟一个溢出指针。
}

二、map初始化 makemap

2.1、源码

func makemap64(t *maptype, hint int64, h *hmap) *hmap {
	if int64(int(hint)) != hint {
		hint = 0
	}
	return makemap(t, int(hint), h)
}

// makemap_small implements Go map creation for make(map[k]v) and
// make(map[k]v, hint) when hint is known to be at most bucketCnt
// at compile time and the map needs to be allocated on the heap.
func makemap_small() *hmap {
	h := new(hmap)
	h.hash0 = fastrand()
	return h
}

// makemap 实现了make的Go map 创建(map[k]v,hint)。
// 如果编译器已经确定可以在堆栈上创建映射或第一个bucket,
// 则h 和/或 bucket可以是非nil。
// 如果h!=nil,map可以直接在h中创建。
// 如果h.buckets!=nil,指向的bucket可以用作第一个bucket。
func makemap(t *maptype, hint int, h *hmap) *hmap {
	mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size) // overflow 内存是否溢出,mem指最坏情况下,一个桶中一个元素,此时所有bucket占用的总内存
	if overflow || mem > maxAlloc { //如果内存溢出,或超过了当前系统的最大内存,则将hit置为0
		hint = 0
	}

	// initialize Hmap
	if h == nil {
		h = new(hmap)
	}
	h.hash0 = fastrand() // 生成随机HASH种子

	// 查找大小参数B,该参数将包含请求的元素数。
	// 对于提示<0,overLoadFactor 返回false,因为提示 < bucketCnt。
	B := uint8(0)
	for overLoadFactor(hint, B) { // hint > 8 并且 hint > 6.5 * 2^B
		B++
	}
	h.B = B

	// 创建初始哈希表
	// 如果B==0,buckets字段稍后会延迟分配(在新增/修改 mapassign 中)
	// 如果提示很大,则归零此内存可能需要一段时间。
	if h.B != 0 {
		var nextOverflow *bmap
		h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) // 初始化桶,可能会提前分配溢出桶
		if nextOverflow != nil {// 如果预分配了空闲的溢出桶数组,则初始化 mapextra 字段,用 h.extra.nextOverflow 指向第一个溢出桶
			h.extra = new(mapextra)
			h.extra.nextOverflow = nextOverflow
		}//否则代表没有溢出桶,h.extra.nextOverflow指向nil
	}

	return h
}

2.2、解释

  1. 校验count是否导致内存溢出或超过当前系统允许的最大内存,如果超过,将count置为0
  2. 初始化hmap结构体
  3. 计算哈希种子hash0,用于后期计算key的哈希时使用
  4. 根据负载因子(6.5)和count计算B(负载因子*2^B>=count)
  5. 如果B=0,初始化结束,如果B>0,创建普通桶和部分溢出桶
  6. 当count>52(B>3)时,创建溢出桶,否则不创建溢出桶。如果创建溢出桶,溢出桶数量至少为:2^(B-4)次方个
  7. 普通桶和溢出桶组成一个数组返回,指向hmap.bucket指针
  8. 如果创建了溢出桶,则将第一个空闲溢出桶指向hmap.extra.nextOverflow字段,最后一个空闲溢出桶的overflow指向第一个普通桶

三、map新增/更新 mapassign

3.1、源码

// 插入和添加函数,类似于 mapaccess1、 mapaccess2,但如果密钥不在映射中,则为其分配一个插槽。
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	if h == nil {//非空判断,如果往一个未初始化的map中插入数据,就会抛出该异常
		panic(plainError("assignment to entry in nil map"))
	}
	if raceenabled {
		callerpc := getcallerpc()
		pc := abi.FuncPCABIInternal(mapassign)
		racewritepc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.key, key, callerpc, pc)
	}
	if msanenabled {
		msanread(key, t.key.size)
	}
	if asanenabled {
		asanread(key, t.key.size)
	}
	if h.flags&hashWriting != 0 {
		fatal("concurrent map writes")
	}
	hash := t.hasher(key, uintptr(h.hash0))

	// Set hashWriting after calling t.hasher, since t.hasher may panic,
	// in which case we have not actually done a write.
	h.flags ^= hashWriting // 任何数和0做异或都是任何数

	if h.buckets == nil {//如果该map中没有桶,就new一个,一般初始化方式(make(map[string]string))不指定map长度时,会触发该逻辑
		h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
	}

again:
	bucket := hash & bucketMask(h.B)//根据哈希值的低B位得到目标桶的下标(这里bucket可能是扩容后的桶的下标)
	if h.growing() {//如果map正在扩容,则先迁移,再继续添加操作
		growWork(t, h, bucket)
	}
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))//获取目标桶对象
	top := tophash(hash)//获取哈希值的高8位,后续用来找槽

	var inserti *uint8 // 待插入的下标位置的地址
	var insertk unsafe.Pointer //待插入的key位置的地址
	var elem unsafe.Pointer //待插入的value位置的地址
bucketloop:
	for {
		for i := uintptr(0); i < bucketCnt; i++ {//遍历8个槽
			if b.tophash[i] != top {//当前槽的top哈希不等于top
				if isEmpty(b.tophash[i]) && inserti == nil {//该槽的tophash是空,说明这个槽是空的
					inserti = &b.tophash[i]//设置 待插入的tophash位置的地址
					insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))//设置 待插入的key位置的地址
					elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))//设置 待插入的value位置的地址
				}
				if b.tophash[i] == emptyRest {//哈希不相等,并且该槽是目标桶和溢出桶中的最后一个槽
					break bucketloop//如果当前要插入的槽是map最后一个槽,该槽后边都是空的槽,说明没有找到哈希相等的位置,则结束循环
				}
				continue
			}//下边是top哈希相等的情况
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))//获取到 待插入的key位置的地址
			if t.indirectkey() {
				k = *((*unsafe.Pointer)(k))
			}
			if !t.key.equal(key, k) {//待插入的key值和当前位置的key值不相等(哈希冲突了),继续下一次循环
				continue
			}
			// 以下是修改操作:找到相同的key值,已经具有键的映射。更新它。
			if t.needkeyupdate() {//key的哈希和key值都相等,所以当前操作是更新操作
				typedmemmove(t.key, k, key)
			}
			elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))//获取待插入的elem位置的地址
			goto done
		}
		ovf := b.overflow(t)
		if ovf == nil {
			break
		}
		b = ovf
	}

	// 找不到键的映射。分配新单元格并添加条目。

	// 如果我们达到了最大负载因子,或者我们有太多的溢出桶,
	// 而我们还没有处于增长之中,那么就开始增长。
	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 // 增大表会使所有内容无效,请重试
	}

	if inserti == nil {// 当前bucket和与其连接的所有溢出bucket都已满,请分配一个新的bucket。
		// The current bucket and all the overflow buckets connected to it are full, allocate a new one.
		newb := h.newoverflow(t, b)
		inserti = &newb.tophash[0]
		insertk = add(unsafe.Pointer(newb), dataOffset)
		elem = add(insertk, bucketCnt*uintptr(t.keysize))
	}

	// store new key/elem at insert position // 将新key/elem存储在插入位置
	if t.indirectkey() {//如果key太大,需要存储为指针类型时
		kmem := newobject(t.key)//创建key对象
		*(*unsafe.Pointer)(insertk) = kmem
		insertk = kmem	//将槽中的key指向新的key内存
	}
	if t.indirectelem() {//如果elem需要保存为指针类型
		vmem := newobject(t.elem)
		*(*unsafe.Pointer)(elem) = vmem 	//将槽中的elem指向新的elem内存
	}
	typedmemmove(t.key, insertk, key)//如果insertk已经指向了新key的地址,则该函数结束,否则将key值赋值给insertk
	*inserti = top 		//inserti保存top哈希的地址
	h.count++ //map元素数量加1

done:
	if h.flags&hashWriting == 0 {
		fatal("concurrent map writes")
	}
	h.flags &^= hashWriting //重置flags标志位
	if t.indirectelem() {//获取elem的值
		elem = *((*unsafe.Pointer)(elem))
	}
	return elem
}

3.2、解释

  1. 非nil判断,如果map没有初始化,则抛出 assignment to entry in nil map 异常
  2. 并发写写判断,如果此时有另一个协程在写map,则抛出concurrent map writes 异常
  3. 根据key值和哈希种子(hash0)计算key的hash。
  4. 将map的标志位flags置为写,表示当前有线程在执行写操作
  5. 如果此时map中没有桶,先新建一个桶
  6. 根据哈希值的低B位获得要插入位置的目标桶
  7. 如果当前正在扩容,则先扩容:迁移一到两个桶数据到新桶
  8. 根据哈希的高8位获取tophash
  9. 遍历目标桶和与目标桶相连的溢出桶中的8个槽
  10. 如果所有槽的tophash都不相等,如果有空槽,则记录下当前槽的k/v地址位置
  11. 如果有某个槽的tophash相等,再比较对应tophash位置的key值是否相等
  12. 如果tophash相等,key值相等,则为更新操作:插入key和value,结束。
  13. 如果tophash和key值不同时相等,但有空槽,则往第一个空槽中插入key/value,更新count值,结束。
  14. 如果tophash和key值不同时相等,也没有空槽,则先新建一个溢出桶,再往第一个空槽中插入key/value,更新count值,结束。

四、map查找 mapaccess1

4.1、源码

// mapaccess1 返回一个指向h[key]的指针。从不返回nil,
// 相反,如果键不在映射中,它将返回对elem类型的zero对象的引用。
//
// NOTE: 返回的指针可能会使整个map保持活动状态,(因为反获得是指针,所以整个map不能被GC)
// 所以不要长时间保持它。
func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := abi.FuncPCABIInternal(mapaccess1)
		racereadpc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.key, key, callerpc, pc)
	}
	if msanenabled && h != nil {
		msanread(key, t.key.size)
	}
	if asanenabled && h != nil {
		asanread(key, t.key.size)
	}
	if h == nil || h.count == 0 {//map为nil或者map为空时,不报错,直接返回零值
		if t.hashMightPanic() {
			t.hasher(key, 0) // see issue 23734
		}
		return unsafe.Pointer(&zeroVal[0])
	}
	if h.flags&hashWriting != 0 {//map不可以并发读写
		fatal("concurrent map read and map write")//并发读和并发写冲突
	}
	hash := t.hasher(key, uintptr(h.hash0)) // 计算key的哈希值,并且加入 hash0 引入随机性,
	m := bucketMask(h.B)	//m = 2^B - 1
	b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))//目标桶的首地址
	if c := h.oldbuckets; c != nil {
		if !h.sameSizeGrow() {
			// There used to be half as many buckets; mask down one more power of two.
			m >>= 1
		}
		oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
		if !evacuated(oldb) {
			b = oldb
		}
	}
	top := tophash(hash)//计算key的高8位哈希
bucketloop:
	for ; b != nil; b = b.overflow(t) {//遍历当前桶和所有溢出桶
		for i := uintptr(0); i < bucketCnt; i++ {//遍历桶的数组
			if b.tophash[i] != top {//如果top哈希相等,说明我们要找的key就在这个桶中,否则一定不在这个桶中,循环继续
				if b.tophash[i] == emptyRest {//当前元素为空,虽然后边还有桶,但不会再有元素了
					break bucketloop
				}
				continue
			}//top哈希相等,我们要找的key就在这个桶中
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))//再确定是数组中的哪个key,这里找的key的首地址
			if t.indirectkey() {//如果Key是指针
				k = *((*unsafe.Pointer)(k))//解引用,找到指针对应的key值
			}
			if t.key.equal(key, k) {//比较key值是否相等
				e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))//key值相等,找的对应的元素首地址
				if t.indirectelem() {
					e = *((*unsafe.Pointer)(e))//解引用,获取元素值
				}
				return e
			}
		}
	}
	return unsafe.Pointer(&zeroVal[0])//没有找的key,返回零值
}

4.2、解释

  1. 判断如果hmap为空,或者没有初始化,直接返回零值
  2. 并发读写检查
  3. 根据key和哈希种子(hash0)计算哈希
  4. 根据哈希值的低B位获取目标桶
  5. 根据哈希值的高8位获取top哈希
  6. 遍历目标桶和与目标桶相连的溢出桶中的槽
  7. 比较待查找的key的tophash和槽中的top哈希是否相等,不相等则继续,遍历到最后找不到则直接返回零值
  8. top哈希相等,再比较对应的key值是否相等,key值相等,获取elem值并返回,否则返回零值。

五、map删除 mapdelete

5.1、源码

//删除元素时,运行时调用此函数
func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		pc := abi.FuncPCABIInternal(mapdelete)
		racewritepc(unsafe.Pointer(h), callerpc, pc)
		raceReadObjectPC(t.key, key, callerpc, pc)
	}
	if msanenabled && h != nil {
		msanread(key, t.key.size)
	}
	if asanenabled && h != nil {
		asanread(key, t.key.size)
	}
	if h == nil || h.count == 0 {//当只声明了map或者map中元素为0时,走该逻辑:直接返回
		if t.hashMightPanic() {
			t.hasher(key, 0) // see issue 23734
		}
		return
	}
	if h.flags&hashWriting != 0 {//判断是否有协程正在写,如果有,则抛出并发异常
		fatal("concurrent map writes")
	}
	//根据哈希种子和key计算key 的哈希值
	hash := t.hasher(key, uintptr(h.hash0))

	// 在调用t.hasher之后设置hashWriting,因为t.hasher可能会死机,
	// 在这种情况下,我们实际上还没有进行写入(删除)。 // 我们之所以要把设置标志位放在计算哈希后边,是因为计算哈希可能会死机。
	h.flags ^= hashWriting //增加写标志
	//根据 key的哈希值的低B位 得到第几个桶,bucket是一个指针类型的整数,最小为0,最大为普通桶数量-1
	bucket := hash & bucketMask(h.B)
	if h.growing() {//判断是否正在扩容
		growWork(t, h, bucket) //如果正在扩容,则将目标桶迁移到新地址
	}
	b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))//获取目标桶对象bmap
	bOrig := b//记录初始目标桶
	top := tophash(hash)//获取top哈希
search:
	for ; b != nil; b = b.overflow(t) {//遍历初始桶和溢出桶
		for i := uintptr(0); i < bucketCnt; i++ {//遍历桶中的8个槽
			if b.tophash[i] != top {//比较top哈希,top哈希不相等:如果该槽是最后一个元素,则退出search循环,否则继续遍历下一个槽
				if b.tophash[i] == emptyRest {
					break search
				}
				continue
			}
			k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))//top哈希相等,获取对应的槽的地址
			k2 := k//记录初始槽的地址
			if t.indirectkey() {//如果key为指针
				k2 = *((*unsafe.Pointer)(k2))//解引用,获取指针对应的key值
			}
			if !t.key.equal(key, k2) {//比较key值是否相等,若不相等,循环继续
				continue
			}//程序到这里,top哈希和key都相等,找到了要删除的key,准备删除
			// Only clear key if there are pointers in it.
			if t.indirectkey() {//如果key是指针(本身不是指针,被编译器优化为指针),直接置为nil,由GC释放内存
				*(*unsafe.Pointer)(k) = nil
			} else if t.key.ptrdata != 0 {//key本身为指针时,则手动清除指针内存
				memclrHasPointers(k, t.key.size)
			}
			e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))//获取elem对象的地址
			if t.indirectelem() {
				*(*unsafe.Pointer)(e) = nil //如果elem是指针(本身不是指针,被编译器优化为指针),直接置为nil,由GC释放内存
			} else if t.elem.ptrdata != 0 {
				memclrHasPointers(e, t.elem.size)//elem本身为指针时,则手动清除指针内存
			} else {
				memclrNoHeapPointers(e, t.elem.size)//清除不包含指针的 elem 的内存
			}
			b.tophash[i] = emptyOne//将tophash置为空
			// If the bucket now ends in a bunch of emptyOne states,
			// change those to emptyRest states.
			// It would be nice to make this a separate function, but
			// for loops are not currently inlineable.
			if i == bucketCnt-1 {//如果删除的元素是桶中的最后一个元素,
				if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {//目标桶后边带着溢出桶,且溢出桶中有元素
					goto notLast  // 走该逻辑,并退出search循环
				}
			} else {//如果删除的元素不是桶中的最后一个元素,
				if b.tophash[i+1] != emptyRest {//后边还有元素
					goto notLast // 走该逻辑,并退出search循环
				}
			}
			for {//如果删除的元素是最后一个元素,需要判断该元素前边是否是空,如果是空,置为emptyRest
				b.tophash[i] = emptyRest	//重新设置tophash状态
				if i == 0 { //删除的元素是桶中的第一个元素
					if b == bOrig {//该桶是普通桶
						break // beginning of initial bucket, we're done.
					}
					// Find previous bucket, continue at its last entry. // 删除的元素在溢出桶中
					c := b // 记录c为目标溢出桶
					for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
					}//获取到目标溢出桶的上一个桶,赋值给b
					i = bucketCnt - 1
				} else {
					i--
				}
				if b.tophash[i] != emptyOne {
					break
				}
			}
		notLast:
			h.count--//map的count数量减1
			// Reset the hash seed to make it more difficult for attackers to
			// repeatedly trigger hash collisions. See issue 25237.
			if h.count == 0 {// 如果删除之后,map中元素数量为0,则重置哈希种子,使攻击者更难重复触发哈希冲突。
				h.hash0 = fastrand()
			}
			break search
		}
	}

	if h.flags&hashWriting == 0 {//有其他线程在操作,需要抛出并发写异常检查
		fatal("concurrent map writes")
	}
	h.flags &^= hashWriting //将flag标志置为0
}

5.2、解释

  1. 判断如果hmap为空,或者没有初始化,程序结束
  2. 并发 写写 检查
  3. 增加写标志位
  4. 根据key和哈希种子(hash0)计算哈希
  5. 根据哈希值的低B位获取目标桶地址
  6. 根据哈希值的高8位获取tophash
  7. 循环遍历目标桶和与目标桶相连的溢出桶的槽
  8. 先比较top哈希,如果待删除的top哈希和槽中的top哈希不相等,程序结束
  9. 如果top哈希相等,再比较key值,key值相等,则删除,否则程序结束
  10. 删除key/elem之后,将当前槽的tophash置为emptyone(1),表示该槽为空,但后边可能还有数据
  11. 当目标槽后边还有元素,则count–,程序结束
  12. 如果目标槽是最后一个元素,则将该槽的tophash置为emptyrest(0),
  13. 再判断目标槽上一个槽是否是空,如果是空,也置为emptyrest(0),直到遍历到普通桶的第一个元素或者遍历到不为空的槽后,程序结束。

5.3、map新建溢出桶 newoverflow

//新建溢出桶
func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {
	var ovf *bmap
	if h.extra != nil && h.extra.nextOverflow != nil {//使用初始化时创建的空闲溢出桶
		// We have preallocated overflow buckets available.
		// See makeBucketArray for more details.
		ovf = h.extra.nextOverflow
		if ovf.overflow(t) == nil {
			// We're not at the end of the preallocated overflow buckets. Bump the pointer.
			h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))
		} else {
			// This is the last preallocated overflow bucket. // 这是最后一个预先分配的溢出存储桶。
			// Reset the overflow pointer on this bucket,
			// which was set to a non-nil sentinel value.
			ovf.setoverflow(t, nil)
			h.extra.nextOverflow = nil
		}
	} else {//没有了空闲溢出桶,新建溢出桶
		ovf = (*bmap)(newobject(t.bucket))
	}
	h.incrnoverflow()//计算溢出桶数量
	if t.bucket.ptrdata == 0 {//当前桶的key/elem不包含指针
		h.createOverflow()
		*h.extra.overflow = append(*h.extra.overflow, ovf)
	}
	b.setoverflow(t, ovf)//当前桶指向溢出桶
	return ovf
}
  1. 添加元素和扩容时触发
  2. 首先检查hmap.extra.nextOverflow是否有空闲溢出桶
  3. 如果有,则获取到该空闲溢出桶,如果该桶是最后一个空闲桶,则将hmap.extra.nextOverflow指向nill,并 将该空闲桶.overflow指向nil,否则hmap.extra.nextOverflow指向下一个空闲桶
  4. 如果没有,则新建溢出桶
  5. 更新hmap.noverflow(溢出桶数量)
  6. 如果当前桶的k/v不包含指针,则初始化hmap.extra.overflow,并追加当前创建的溢出桶。即hmap.extra.overflow保存的是溢出桶
  7. 将当前桶的overflow指向溢出桶,结束

六、map扩容1-hashGrow

6.1、源码

//扩容操作:只有新增才会触发扩容	1、分配原内存两倍的内存空间;2、将桶中的数据地址挂到oldbuckets上。
func hashGrow(t *maptype, h *hmap) {
	// 如果我们已经达到了负载系数,就做大。
	// 否则,溢出的桶太多,
	// 所以保持相同的桶数并横向“增长”。
	bigger := uint8(1)
	if !overLoadFactor(h.count+1, h.B) {//不需要新增桶
		bigger = 0
		h.flags |= sameSizeGrow //记录标志位 等量扩容 或运算符在这里等效为加法运算 即4+8=12
	}
	oldbuckets := h.buckets // 将桶中的数据地址指向旧桶字段
	newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)//创建新的桶和新的溢出桶

	flags := h.flags &^ (iterator | oldIterator) // 12
	if h.flags&iterator != 0 {//当前map正在迭代
		flags |= oldIterator
	}
	// commit the grow (atomic wrt gc)
	h.B += bigger // B的值+1
	h.flags = flags
	h.oldbuckets = oldbuckets
	h.buckets = newbuckets
	h.nevacuate = 0
	h.noverflow = 0

	if h.extra != nil && h.extra.overflow != nil {
		// Promote current overflow buckets to the old generation.
		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() 增量完成的。
}

6.2、解释

  1. 添加元素时触发,更新元素不触发
  2. 触发扩容的条件有两个:第一是负载因子超过阈值,这里阈值是6.5。当元素数量>6.5*桶数量的时候会触发扩容,此时触发的是两倍扩容。
    第二是溢出桶数量过多。当普通桶总数2B小于215时,如果溢出桶数量大于等于普通桶数量就会触发扩容。反之,当普通桶总数2B大于等于215时,如果overflow的bucket数量超过2^15,也会触发扩容 。此时触发的是等量扩容。
  3. 如果达到了负载因子,就进行两倍扩容:B=B+1;如果没有,就进行等量扩容:B不变。
  4. 将hmap.bucket的桶的地址指向oldbucket
  5. 创建等量/两倍数量的新桶和一些溢出桶
  6. 将hmap.noverflow(溢出桶大致数量)置为零,hmap.bucket指向新桶等
  7. 将hmap.extra.nextOverflow指向第一个空闲溢出桶(如果有)

七、map扩容2-growWork

删除操作、新增/修改操作会触发扩容2操作

7.1、源码

func growWork(t *maptype, h *hmap, bucket uintptr) {
	// 确保我们疏散与我们将要使用的桶相对应的旧桶
	//
	evacuate(t, h, bucket&h.oldbucketmask())//bucket&h.oldbucketmask() == 目标桶在旧桶的下标位置

	// 再疏散一个老水桶,在进度上取得进展
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}

7.2、解释

第一步:将目标桶从旧桶迁移到新桶
  1. 先判断该桶是否需要迁移:判断该桶中第一个槽的tophash是否等于迁移的标志位
  2. 初始化一个evacDst结构体数组,长度为2,evacDst结构体:迁移目的地,即保存迁移后的新桶位置的指针,包括桶/key/elem三个。evacDst结构体数组的第一个是新桶的前半部分指针,第二个元素是新桶的后半部分指针。
  3. 遍历目标桶和与桶连接的溢出桶,获取到目标桶中第一对key/elem,再遍历每个桶中的槽
  4. 获取槽的tophash,如果tophash标志位是空,就将该槽的tophash标志为已迁移,程序继续,直到槽不是空的时候继续往下执行。
  5. 根据tophash得知当前槽不是空槽,获取到该槽的key值
  6. 判断:如果是等量扩容,那将当前槽的tophash标志迁移到前半部分(evacuatedX),准备将当前槽的数据迁移到新桶的前半部分,
  7. 判断dst.i(dst =evacDst[0])是否大于8,如果大于,则要创建溢出桶,溢出桶重新指向dst。
  8. 将tophash赋值给dst.b.tophash;将key值赋值给dst.k,将elem值赋值给dst.elem
  9. dst.i加1
  10. 更新dst.k和dst.elem指向下一个槽
  11. 第六步如果判断得知目前是非等量扩容,重新计算key的hash
  12. 如果已经存在迭代器,则计算top & 1,如果等于1,那就将当前槽迁移到新桶后半部分,否则迁移到前半部分,再重新计算top
  13. 如果没有迭代器,判断 hash&旧桶数量是否等于0,从而判断出当前槽应该去往新桶的前/后半部分
  14. 然后再执行第7步-10步
  15. 执行到这里,说明目标桶及其溢出桶已经迁移完毕。判断如果当前map有旧迭代器,并且,桶中有指针
  16. 获取到目标桶在旧桶中的指针b,再根据b获取到当前桶中的数据开始位置的指针ptr,再获取到所有数据的大小n,清除从ptr开始到n个字节的内存。
  17. 最后,如果目标桶下标等于hmap.nevacuate(迁移进度计数器)(比如第一次调用扩容2函数,hmap.nevacuate默认是0,目标桶下标也是0,就走该逻辑),hmap.nevacuate加1,表示又迁移了一个桶,
  18. 如果hmap.nevacuate不等于旧桶数量(说明hmap.nevacuate还需要计数),并且,该桶已经迁移了,执行hmap.nevacuate加1。
  19. 如果hmap.nevacuate等于旧桶数量(所有桶迁移完毕)。是否旧桶内存,hmap.oldbuckets=nil,并将hmap.extra.oldoverflow = nil。去掉等量扩容标志位.结束
第二步:如果还没有迁移完毕,就再迁移一个桶,这个桶是hmap.nevacuate位置的桶
  1. 如果该桶需要迁移,则执行第一步步骤,否则直接更新迁移进度标志位,详细步骤见第一步。

如果第一次调用扩容2方法,

  • 第一步:目标桶下标大于0,则只迁移目标桶,不更新hmap.nevacuate
  • 第二步:迁移hmap.nevacuate,此时hmap.nevacuate=0,迁移之后hmap.nevacuate=1

八、map迭代器-初始化-mapiterinit

8.1、源码

// mapiterinit 初始化用于对映射进行测距的hiter结构。
// “it”指向的hiter结构由编译器order pass在堆栈上分配,
// 由  reflect_mapiterinit 在堆上分配。
// 或由于结构包含指针,因此两者都需要具有零hiter。
func mapiterinit(t *maptype, h *hmap, it *hiter) {
	if raceenabled && h != nil {
		callerpc := getcallerpc()
		racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapiterinit))
	}

	it.t = t
	if h == nil || h.count == 0 {
		return
	}

	if unsafe.Sizeof(hiter{})/goarch.PtrSize != 12 {
		throw("hash_iter size incorrect") // see cmd/compile/internal/reflectdata/reflect.go
	}
	it.h = h

	// 获取bucket状态的快照
	it.B = h.B
	it.buckets = h.buckets
	if t.bucket.ptrdata == 0 {//桶中没有指针
		// 分配当前切片并记住指向当前和旧切片的指针。
		// 这将保持所有相关的溢出存储桶处于活动状态,
		// 即使在我们迭代时表增长和/或溢出存储桶添加到表中也是如此。
		//
		h.createOverflow()
		it.overflow = h.extra.overflow
		it.oldoverflow = h.extra.oldoverflow
	}

	// 决定从哪里开始
	var r uintptr // r代表随机数
	if h.B > 31-bucketCntBits {
		r = uintptr(fastrand64())
	} else {
		r = uintptr(fastrand())
	}
	it.startBucket = r & bucketMask(h.B)
	it.offset = uint8(r >> h.B & (bucketCnt - 1))

	// 迭代器状态
	it.bucket = it.startBucket

	// 记住我们有一个迭代器。
	// 可以与另一个 mapiterinit() 同时运行。
	if old := h.flags; old&(iterator|oldIterator) != iterator|oldIterator {
		atomic.Or8(&h.flags, iterator|oldIterator)
	}

	mapiternext(it)
}

8.2、解释

整个的遍历过程是基于hiter结构体来遍历的,初始化过程就是初始化hiter结构体

  1. 如果map没有初始化,或者,map中元素为0,直接返回。
  2. 将map指针赋值给hiter.hmap(需要迭代的map)
  3. 将map.B赋值给hiter.B;map.buckets赋值给hiter.buckets,记录此时的map快照数据。
  4. 计算出一个随机数r,来决定从哪里开始遍历,这也是为什么map无序的原因所在
  5. r & map的后B位得到的值赋值给hiter.startBucket,startBucket指遍历开始的桶。
  6. 再计算出随机开始的bucket的槽,赋值给hiter.offset,offset最大为7,最小为0.
  7. 将hiter.startBucket赋值给hiter.bucket,记录当前正在遍历的bucket。
  8. hiter初始化完成,h,flags标记迭代器标识,迭代器可以多个同时运行。
  9. 最后调用mapiternext函数

九、map迭代器-遍历-mapiternext

9.1、源码

// mapiternext 迭代器 执行
func mapiternext(it *hiter) {
	h := it.h
	if raceenabled {
		callerpc := getcallerpc()
		racereadpc(unsafe.Pointer(h), callerpc, abi.FuncPCABIInternal(mapiternext))
	}
	if h.flags&hashWriting != 0 { // 检查并发写/遍历
		fatal("concurrent map iteration and map write")
	}
	t := it.t
	bucket := it.bucket
	b := it.bptr
	i := it.i
	checkBucket := it.checkBucket

next:
	if b == nil { //可能刚开始遍历,或者,刚遍历完上个桶及其溢出桶
		if bucket == it.startBucket && it.wrapped { // 遍历到了第一个桶,并且转了一圈,说明全部遍历了一遍
			// end of iteration
			it.key = nil
			it.elem = nil
			return
		}
		if h.growing() && it.B == h.B {	// 1.初始化之前扩容,此时hiter.buckets指向的是新桶;2.初始化之后扩容,此时hiter.buckets指向的是旧桶,并且是等量扩容
			// 迭代程序是在增长过程中启动的,但增长尚未完成。
			// 如果我们正在查看的bucket尚未填充(即,旧bucket尚未排空),
			// 那么我们需要遍历旧bucket,只返回将迁移到此bucket的bucket。
			//
			oldbucket := bucket & it.h.oldbucketmask()
			b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
			if !evacuated(b) {	// 当前桶没有被迁移,将当前桶下标指向checkBucket,checkBucket代表要检查的桶
				checkBucket = bucket
			} else {	// 当前桶已经被迁移
				b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
				checkBucket = noCheck
			}
		} else {	// 1.初始化之后开始扩容,并且是非等量扩容,但是hiter.buckets指向的是旧桶的地址;2.没有进行扩容
			b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
			checkBucket = noCheck
		}
		bucket++
		if bucket == bucketShift(it.B) {	// 说明遍历到了末尾
			bucket = 0
			it.wrapped = true 		// 标志从末尾遍历到了开头
		}
		i = 0
	}
	for ; i < bucketCnt; i++ {
		offi := (i + it.offset) & (bucketCnt - 1) // 随机获取起始槽
		if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty { //槽已经被迁移了,或者该槽为空,则继续遍历下一个槽
			// TODO: emptyRest在这里很难使用,因为我们开始在bucket的中间迭代。
			// 这是可行的,只是很棘手。
			continue
		}
		k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize)) // 获取随机槽的key
		if t.indirectkey() {
			k = *((*unsafe.Pointer)(k))
		}
		e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize)) // 获取槽中的elem
		if checkBucket != noCheck && !h.sameSizeGrow() {	// 迭代初始化之前进行的扩容,并且当前桶还没有迁移(还在旧桶中),并且是非等量扩容
			// Special case: 迭代器是在增长到更大的大小期间启动的,但增长尚未完成。
			// 我们正在处理一个旧桶还没有被迁移的桶。
			// 或者至少,当我们启动水桶时,它没有被迁移。
			// 因此,我们遍历旧bucket,
			// 跳过任何将转到另一个新bucket的键
			// (在增长过程中,每个旧bucket扩展到两个bucket)。
			// 非等量扩容。
			if t.reflexivekey() || t.key.equal(k, k) { //如果k是可比较的
				// 如果旧存储桶中的项目不是迭代中当前新存储桶的目标,
				// 请跳过它。
				hash := t.hasher(k, uintptr(h.hash0))
				if hash&bucketMask(it.B) != checkBucket { //当前桶在新map的后半部分,则继续循环
					continue
				}
			} else {
				// 如果k!=k,则哈希不可重复k(NaNs)。
				// 我们需要一个可重复和随机的选择,
				// 在迁移期间向哪个方向发送NaN。
				// 我们将使用tophash的低位来决定NaN的走向。
				// NOTE: 这种情况就是为什么我们需要两个排空tophash值,
				// 排空的X和排空的Y,它们的低位不同。
				// 如果迁移到是后半部分,就继续循环
				if checkBucket>>(it.B-1) != uintptr(b.tophash[offi]&1) {
					continue
				}
			}
		}
		if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
			!(t.reflexivekey() || t.key.equal(k, k)) {
			// 这是黄金数据,我们可以返回。
			// OR
			// key!=key, 所以条目不能被删除或更新,所以我们可以直接返回它。
			// 这对我们来说是幸运的,因为key!=key时我们无法成功查找。
			it.key = k
			if t.indirectelem() {
				e = *((*unsafe.Pointer)(e))
			}
			it.elem = e
		} else {
			// 自迭代器启动以来,哈希表一直在增长。
			// 这个密钥的黄金数据现在在其他地方。
			// 请检查当前哈希表中的数据。此代码处理key已被删除、更新或删除并重新插入的情况。
			//
			//
			// NOTE: 我们需要重新标记密钥,
			// 因为它可能已更新为equal()但不相同的密钥(例如+0.0 vs-0.0)。
			rk, re := mapaccessK(t, h, k)
			if rk == nil {
				continue // key已经被删除
			}
			it.key = rk
			it.elem = re
		}
		it.bucket = bucket
		if it.bptr != b { // 避免不必要的写入障碍; see issue 14921
			it.bptr = b
		}
		it.i = i + 1
		it.checkBucket = checkBucket
		return
	}
	b = b.overflow(t) //继续遍历溢出桶
	i = 0
	goto next
}

9.2、解释

遍历的大致过程:随机找一个桶,随机找桶中的槽,依次遍历槽和溢出桶,和其他的普通桶

  1. 根据hiter.hmap.flags判断当前是否有写操作,如果有,抛出并发迭代/写异常,即map不支持并发写和遍历
  2. 开始遍历。如果hiter.bptr(当前遍历的桶对象)等于nil,进入if语句。
  3. 如果hiter.bucket等于hiter.startBucket,并且,it.wrapped等于true,说明已经从map的开始绕到了开头,此时应该结束遍历,并且将hiter.key/elem置为nil
  4. 如果当前正在进行等量扩容(旧桶不为nil,hiter.B等于hmap.B)
  5. 如果(1)初始化之前就已经在扩容了,此时hiter.buckets指向的是新桶;(2)初始化之后才开始扩容,并且是等量扩容:
  6. 从旧桶中获取到当前要遍历的桶的指针
  7. 如果当前桶没有被迁移,将当前桶在新桶中的坐标bucket指向checkBucket。
  8. 如果当前桶已经被迁移,则获取当前桶在新桶的指针,赋值给b。
  9. 回到第5步,如果(1)初始化之后才开始扩容,并且是非等量扩容,但是hiter.buckets指向的是旧桶的地址;(2)没有在扩容
  10. 直接在hiter.buckets中获取当前桶b的地址即可
  11. 指向bucket++(当前遍历的桶加1),i=0(i是用来遍历槽的).
  12. 如果bucket等于普通桶数量(1<<B),说明已经从桶的末尾绕道了开头,将bucket置为0,下一次遍历第一个普通桶。

总结

喜欢的收藏点赞加关注哦,敬请期待下一篇博客带大家手写map。
有任何描述的不对的地方,欢迎指出,我会及时改正。

  • 19
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

才华横溢caozy

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值