Go数据结构之映射map方式
作者:大P哥阿豪
Map作为Go原生支持的另一个主要的数据结构,其使用频繁程度与切片有过之而无不及。用一句话描述map:存储键-值对映射关系的无序数据结构,保证键的唯一性但不保证并发操作安全。
本文将介绍map的基本使用以及go 1.16版本其底层实现。
1 map的使用
1.1 map基本使用
声明/初始化,一般有如下三种方式,
- 使用var声明一个map变量,需要指定键值对分别的类型,这种方式仅仅只是声明,返回的是一个nil map,既没有分配内存,无法对其进行写入操作。如果要对其进行写入操作,则还需要为其分配内存。
- 比较常用的是直接使用make初始化,在初始化map类型时,可以传一个容量(通常是不传这个参数,因为map底层有自动扩容机制,且无法准确预估map所需的容量)。
- 第三种方式就是直接在定义的同时,对map进行写入操作,这种方式适用于确定部分需要写入map的场景
// 声明一个map变量,键是string类型,值是int类型
// 但此时m还是一个nil map,无法对其进行写入
var m map[string]int
m["1"] = 1 // 报错,nil map无法写入
m = make(map[string]int) // 使用make为map分配内存,此时可以正常写入
// 可以将声明和分配内存合二为一,直接使用make函数为map分配内存
// make函数第一个参数是map的类型,第二个参数可选,表示map的容量大小
m1 := make(map[string]int)
// 第三种方式就是在定义的时候,连带着给map进行赋值
m2 := map[string]int{
"1": 1,
"2": 2,
}赋值,其实就比较简单,直接将键放入中括号,对应的值放在等号右边即可,如果键之间存在,则实现更新;不存在,则实现插入。这里需要特别注意的是
- 对于nil map无法进行赋值操作,如果对nil map进行赋值,则会panic。
- map中的value是不可寻址的,无法直接通过map值进行更新操作,但有两种例外,1)value为int类型,编译器会通过语法糖进行直接更新;2)value为指针类型时,也能直接通过map值进行更新操作。
m := make(map[string]int)
m["1"] = 1
type Person struct {
name string
age int
}
m1 := map[string]Person{}
m1["Tom"].age++ // 错误,因为map的值是无法寻址的,这种情况需要接受旧值,将修改完后的值重新赋值
m2 := map[string]*Person{}
m2["Tom"].age++ // 正确,这种情况下没有改变map中的value,而是通过指针找到对应存储的值进行修改- 查找,map对于查询的处理原则就是,如果key不存在,则返回value对应的零值(nil map也能进行查找,只是对于所有的查询都返回value类型零值)。如果需要判断key是否存在,则推荐使用v, ok := m["1"]的写法,ok为true表示key存在于map中。
var m map[string]int
fmt.Println(m["1"]) // nil map可以进行查找,返回的是value的默认零值
// 如果需要判断key是否存在于map中,则可以使用如下写法
if _, ok := m["1"]; ok {
fmt.Println("key存在")
} else {
fmt.Println("key不存在")
}- 删除,主要通过调用delete函数实现map中键值对的删除操作,对于nil map也能执行删除操作,如果待删除的key不存在,则不做任何处理。
var m map[string]int delete(m ,"1")
- 遍历,只能通过for range进行map的遍历操作,而且遍历是无序的,每次遍历结果都不一样,这样做:一是为了让使用者不依赖于map的遍历顺序,二也是与map的底层实现有很大关系。实际开发过程中,如果要确定的遍历顺序,往往需要借助切片保存顺序,然后通过遍历切片去map中取值。
m := map[string]int{
"1": 1,
"2": 2,
}
// 无序遍历,每次遍历的顺序都不一样
for k, v := range m {
fmt.Println(k, v)
}1.2 map注意事项
map是一种引用传递类型,其作为函数参数进行传递时,函数内部修改会直接影响调用者。
map遍历是无序的,即每次遍历的结果都不一样,可能是Go的设计者不想使用者依赖与map的遍历顺序性,个人认为这个也是与其底层实现有关,如果要保证有序,则需要维护额外的数据结构,与Go极简的设计原则不符。
map的key必须是支持==、!=运算的数据类型(map的key可以为float类型、chan类型自定义结构体,但不能是func、map、slice,value则可以为任意数据类型)。
因内存访问安全和哈希算法等缘故,字典被设置成“not addressable”,故不能直接修改value的值(实际上就是不允许直接修改map中的值)。如果需要对value进行修改,一般将需要将整个value返回,修改后再重新赋值,或直接将value设置成指针类型(指针能修改的原因是,通过指针修改原结构体中的数据,但没有修改map中保存的数据)。但是,如果map的value为int,那么可以直接修改value,例如:
m := map[string]int{"test":1}
m["test"]++ // 实际上是语法糖
/*
temp := m["test"]
temp += 1
m["test"] = temp
*/map并不是多线程读写安全的,在多线程开发中使用map需要特别小心,解决此问题一般可以使用sync.RWMutex进行保护或直接使用sync.Map。
访问不存在的主键,返回对应key的零值,并不会panic。删除不存在的键值,也不会引发错误。
可对nil的字典进行读、删除操作,但是写会引发panic!即,从nil的字典中,读出来的都是value的默认值;对nil字典进行删除操作,实际不会产生任何效果。
2 map的底层数据结构
在Go 1.16版本中,使用了hash table来实现map(Go 1.24版本已经使用Swiss table作为map的底层实现,后续有空研究下),其底层实现主要借助hmap、bmap,下面详细介绍下这两个数据。
2.1 bmap
bmap是Go map中bucket的抽象,为提高存储效率,每一个 bmap 都能存储 8 个键值对。
当哈希表中存储的数据过多,单个桶已经装满时就会使用 extra.nextOverflow 中桶存储溢出的数据。上述两种不同的桶在内存中是连续存储的,我们在这里将它们分别称为正常桶和溢出桶。
bucket的结构体 bmap 在 Go 语言源代码中的定义只包含一个简单的 tophash 字段,tophash 存储了键的哈希的高 8 位,通过比较不同键的哈希的高 8 位可以减少访问键值对次数以提高性能。在运行期间,bmap 结构体其实不止包含 tophash 字段,因为哈希表中可能存储不同类型的键值对,而且 Go 语言也不支持泛型,所以键值对占据的内存空间大小只能在编译时进行推导。runtime.bmap 中的其他字段在运行时也都是通过计算内存地址的方式访问的,所以它的定义中就不包含这些字段,不过我们能根据编译期间的 cmd/compile/internal/gc.bmap 函数重建它的结构:
type bmap struct {
tophash [bucketCnt]uint8
}
// 利用../go/src/cmd/compile/internal/gc/reflect.go文件中的bmap函数反推
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
overflow uintptr
}
桶和溢出桶
- 桶(bucket): 每个桶包含固定数量的键值对,Go 中每个桶默认可以存储 8 对键值对。
- 溢出桶(overflow bucket): 当一个桶已满但仍需插入新的键值对时,会创建一个新的桶作为当前桶的溢出桶。
溢出桶的作用
解决哈希冲突:
- 在理想情况下,每个键会被分配到不同的桶中。然而,在实际应用中,由于哈希函数的特性,多个键可能会被分配到同一个桶中(即哈希冲突)。
- 当一个桶已满且仍然需要存储新的键值对时,Go 会创建一个新的桶作为当前桶的溢出桶,并将新键值对存储在这个溢出桶中。
管理存储空间:
- 溢出桶允许动态扩展 map 的存储容量。通过使用链表结构(由多个溢出桶组成),可以有效地管理超过初始桶容量的数据。
- 这种机制避免了频繁地重新分配和复制整个哈希表,从而提高了性能。
高效查找:
- 即使存在多个溢出桶,Go 的
map实现仍然能够高效地进行键值对的查找。通过遍历主桶及其关联的溢出桶,可以快速找到所需的键值对。 - 溢出桶的设计确保了查找操作在平均情况下仍然是 O(1) 时间复杂度。
2.2 hmap
hmap是Go实现map的底层数据结构,主要用于管理bucket的扩容、元素的查找/删除等,其具体结构如下:
type hmap struct {
count int // 元素个数,调用 len(map) 时,直接返回此值
flags uint8 // 标记,对应 const 中 '// flags' 下的几个状态
B uint8 // buckets 的对数 log_2
noverflow uint16 // overflow 的 bucket 近似数
hash0 uint32 // 计算 key 的哈希的时候会传入哈希函数
buckets unsafe.Pointer // 指向 buckets 数组,大小为 2^B。如果元素个数为0,就为 nil
// 扩容的时候,buckets 长度会是 oldbuckets 的两倍
oldbuckets unsafe.Pointer // 指向扩容前的数组(渐进式扩容)
nevacuate uintptr // 指示扩容进度,小于此地址的 buckets 迁移完成
extra *mapextra //保存溢出桶的信息
}
type mapextra struct {
// 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节)
// 使用 extra 来存储 overflow bucket,这样可以避免 GC 扫描整个 map
// 然而 bmap.overflow 也是个指针。这时候我们只能把这些 overflow 的指针
// 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了
overflow *[]*bmap // overflow 包含的是 hmap.buckets 的 overflow 的 bucket
oldoverflow *[]*bmap // oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow 的 bucket
nextOverflow *bmap // 指向空闲的 overflow bucket 的指针
}
3 map实现源码
3.1 map的初始化
使用make函数创建map:
make(map[k]v, hint)
在 hint <= 8(键值对的个数) 时,会调用 makemap_small 来进行初始化,如果 hint > 8,则调用 makemap。在预估待插入的元素个数小于8或者需要的bucket为0时,Go编译器会采用延迟分配策略,只有在真正往map中写入数据时, 才会分配bucket。
makemap函数主要流程如下:
- 内存安全检查:通过计算预估内存 = 元素数量(hint) × 单个桶大小(t.bucket.size),防止内存溢出攻击和无效分配,若检测失败,将 hint 重置为 0(后续按最小分配处理)
- 初始化 hmap 结构体:若传入
h为 nil,新建hmap结构(map 的底层表示);随后h.hash0 = fastrand():生成随机哈希种子,用于增加哈希随机性,防止哈希碰撞攻击,相同 key 在不同 map 中产生不同哈希值 - 计算桶数量指数 B:根据负载因子确定合适的桶数量(
2^B个桶),循环增加B直到满足:6.5 * 2^B >= hint - 分配桶数组:若
B == 0(hint=0),延后到首次写入时分配;其他情况,调用makeBucketArray分配主桶数组(长度为2^B),如果 B >= 4 额外预分配溢出桶(减少运行时分配开销,这里也说明正常桶与溢出桶是内存地址连续的数组) - 溢出桶管理:若存在预分配溢出桶(
nextOverflow != nil),初始化h.extra结构,记录可用溢出桶链表。最后一个溢出桶的overflow指针会指向第一个正常桶,以此形成一个环。
func makemap_small() *hmap {
h := new(hmap)
h.hash0 = fastrand()
return h
}
func makemap(t *maptype, hint int, h *hmap) *hmap {
// 进行内存大小的检查,如果溢出或者内存超出最大内存空间,将hint(元素个数)设置为0
mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
if overflow || mem > maxAlloc {
hint = 0
}
// 初始化 Hmap,即如果当前Hamp为空,则new hmap
// 设置map的哈希计算种子随机数hash0
if h == nil {
h = new(hmap)
}
h.hash0 = fastrand()
// Find the size parameter B which will hold the requested # of elements.
// For hint < 0 overLoadFactor returns false since hint < bucketCnt.
// 按照提供的元素个数,根据负载因子计算合理的 B 值,以确保 6.5 * 2 ^ B >= hint
B := uint8(0)
for overLoadFactor(hint, B) {
B++
}
h.B = B
// allocate initial hash table
// if B == 0, the buckets field is allocated lazily later (in mapassign)
// If hint is large zeroing this memory could take a while.
// 初始化 hash table
// 如果 B 等于 0,那么 buckets 就会在赋值( mapassign )的时候再分配
// 如果长度比较大,分配内存会花费长一点
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
}
func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
// uintptr(1) << ( b & (sys.PtrSize*8-1)) 2^b
base := bucketShift(b)
nbuckets := base
// 当桶的数量小于2^4 时,由于数据较少、使用溢出桶的可能性较低,
// 这时就会省略创建溢出桶的过程以减少额外开销
if b >= 4 {
// 当桶的数量多于2^4时,就会额外创建 2^(b-4)个溢出桶
// 根据内存的分配规则,计算出合适的内存大小,并确定桶的数量
nbuckets += bucketShift(b - 4)
sz := t.bucket.size * nbuckets
up := roundupsize(sz)
if up != sz {
nbuckets = up / t.bucket.size
}
}
// 如果桶之前没有分配内存,就初始化一个数组
// 反之,直接沿用之前的,并清理掉本次初始化所需要内存大小的内存,备用
if dirtyalloc == nil {
buckets = newarray(t.bucket, int(nbuckets))
} else {
// dirtyalloc was previously generated by
// the above newarray(t.bucket, int(nbuckets))
// but may not be empty.
buckets = dirtyalloc
size := t.bucket.size * nbuckets
if t.bucket.ptrdata != 0 {
memclrHasPointers(buckets, size)
} else {
memclrNoHeapPointers(buckets, size)
}
}
// 如果创建的桶数量不等于2^b,说明分配了额外的溢出桶
if base != nbuckets {
// 2^b个桶后面就是溢出桶
nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
// 取nextOverflow 里面的最后一个元素,并把最后一个buckets 的末尾偏移的指针指向空闲的bucket (目前就是第一个buckets了)
last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
last.setoverflow(t, (*bmap)(buckets))
}
return buckets, nextOverflow
}下图是不同B值时,初始化得到的map,如果B小于4,则编译器不会为map分配溢出桶(认为map的规模比较小,需要使用到溢出桶的概率不大);只有在B大于等于4时,才会为map分配 2^(B-4)个溢出桶,需要注意的是正常桶与溢出桶在底层是一个内存地址连续的数组!

3.2 map的赋值
map的赋值操作核心是:通过hash函数,找到key插入到bmap数据的下标索引,然后就需要遍历该下标包含的所有bucket,寻找第一个能插入的位置或者寻找该key是否已经存在。hash值在这里的主要作用有两个:
- tophash:由于一个bucket存储了8个键值对,为了快速比较key,编译器会将hash值的前8位(64位操作系统)存储到bucket到tophash数组。
- 确定bmap的索引:hash值与map的B值进行mask操作,确定该key存储的下标位置。

赋值操作底层mapassign函数的主要流程:
1. 安全检查和初始化
- 空 map 检查:对 nil map 赋值会 panic
- 并发写检查:检测到其他 goroutine 正在写 map 时抛出异常(Go map 非并发安全)
- 哈希计算:使用类型特定的哈希函数计算键的哈希值
- 标记写操作:设置
hashWriting标志位(防止并发写) - 延迟初始化:若桶数组未分配,分配一个桶(最小化空 map 开销)
2. 定位桶和搜索键
双层循环搜索:外层遍历对应index下所有的bucket,内层循环处理每个桶的 8 个槽。
- 遍历时,需要记录第一个可以插入的位置。
- 同时,需要遍历完全插入位置下,所有的bucket。如果遇到tophash相等,进一步判断key是否一致,key也相等,则更新key的信息并结束循环。
- 遇到
emptyRest(后续全空)时,提前终止搜索。

3. 键不存在时的处理
- 扩容条件:负载因子超标(
count/(2^B) > 6.5);溢出桶过多(noverflow >= 2^min(B, 15))。 - 扩容后重试:桶布局改变需重新定位
- 分配新溢出桶:当所有桶都无空闲槽时,则申请一个溢出桶
4. 收尾工作
- 返回值的存储位置:调用方通过此指针写入值
- 间接值处理:若值类型为指针,返回实际值地址
/*
t *maptype:map 的类型信息
h *hmap:map 的底层结构
key unsafe.Pointer:要插入或更新的键
返回:指向值存储位置的指针(写入值需通过此指针)
*/
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
// 对于nil的map不能进行写操作,直接panic
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)
}
// 如果有别的goroutine正在写此map,即发生了并发写,直接异常退出
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 计算需要写入的key的hash值
hash := t.hasher(key, uintptr(h.hash0))
// 调用hasher函数时,可能发生paninc,因此没法完成一次写操作
// 如果hasher没有发生panic,那么将flags设置成flags += 4
h.flags ^= hashWriting
// 如果bucket没有被分配内存,则分配一个bucket(延迟初始化)
if h.buckets == nil {
h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
}
again:
// 计算低 8 位 hash,根据计算出的 bucketMask 选择对应的 bucket 编号
bucket := hash & bucketMask(h.B)
// 如果map正在扩容,则完成搬迁工作
if h.growing() {
growWork(t, h, bucket)
}
// 计算key对应桶编号的地址,以及hash值的高8位
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
top := tophash(hash)
var inserti *uint8 // 需要写入tophash数组的下标
var insertk unsafe.Pointer // 写入key的对象指针(地址)
var elem unsafe.Pointer // 写入value的对象指针(地址),即返回值
bucketloop:
// 开启双层循环,外层遍历bucket的所有overflow桶,内层遍历每个bucket的cell
// 目的:找到空的cell(key不存在),或者top所在的位置(key已存在)
for {
for i := uintptr(0); i < bucketCnt; i++ {
if b.tophash[i] != top { // 当前top不一致,继续比对下一个
// 找到第一个空的cell,并保存下表以及地址信息
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))
}
// 此cell为空且后面也都是空cell,那么inserti必定已不为空。
// 这种情况直接退出bucket cell的遍历
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// 如果b.tophash[i] == top,计算key对应的地址
// k是指针对象,解引用
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
// 如果相同的 hash 位置的 key 和要插入的 key 字面上不相等
// 如果两个 key 的首八位后最后八位哈希值一样,就会进行其值比较,算是一种哈希碰撞吧
if !t.key.equal(key, k) {
continue
}
// map已经有此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 里继续找
// overflow 为 nil,说明到了 overflow 链表的末端了,退出外层循环
ovf := b.overflow(t)
if ovf == nil {
break
}
// 赋值为链表的下一个元素,继续循环
b = ovf
}
// 没有找到 key,分配新的空间
// 如果触发了最大的 load factor,或者已经有太多 overflow buckets
// 并且这个时刻没有在进行 growing 的途中,那么就开始 growing
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 {
// The current bucket and all the overflow buckets connected to it are full, allocate a new one.
// 前面在桶里找的时候,没有找到能塞这个 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))
}
// store new key/elem at insert position
if t.indirectkey() { // 如果键的比较大,则直接使用指针存储
kmem := newobject(t.key)
// 二级指针,insertk看作是指向指针的指针
*(*unsafe.Pointer)(insertk) = kmem
insertk = kmem
}
if t.indirectelem() {
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")
}
// flags对hashWriting按位置0,'^='表示按右边hashWriting二进制为1的位置,置0
// 还原写操作之前的flags
h.flags &^= hashWriting
if t.indirectelem() {
// 如果value是个大对象,则bucket中存储的是对象地址unsafe.Pointer,取出存放value对象的地址
elem = *((*unsafe.Pointer)(elem))
}
return elem
}3.3 map的查找
mapaccess1、mapaccess2、mapaccessk是常用的map查找函数,三个函数的主要实现基本类似,主要区别在于函数的返回值不同。
- mapaccess1只有一个value的返回值,v := m[k]时调用,如果k不存在,返回的是value类型对应的零值
- mapaccess2返回value,bool,v, ok := m[k]时调用,如果k不存在,返回是value类型对应的零值,false
- mapaccessk返回key,value,如果k不存在,返回nil,nil
下面主要分析下mapaccess2函数的实现:
安全性检查
- 空 map 处理:如果 map 为 nil 或元素数为 0,直接返回零值和 false
- 特殊处理:如果键类型可能引发 panic(如函数类型),调用哈希函数触发 panic
- 读写冲突检测:当其他 goroutine 正在写 map 时抛出异常(Go map 非并发安全)
计算哈希和定位桶
bucketMask(h.B):生成用于取模的掩码(如 B=3 时,mask=0b111)- 桶定位公式:桶地址 = buckets起始地址 + (hash & mask) * 桶大小
扩容期间的特殊处理:当 map 正在扩容时,数据可能存在于新旧桶中(扩容有等量扩容与双倍扩容两种,详细解释见3.4节),如果是双倍扩容,且该下标还没有迁移完成,则在老桶中查找
双层循环搜索键值对
- tophash 比较:先比较哈希高8位,快速过滤不匹配项
- emptyRest 优化:遇到标记为
emptyRest的槽位,表示后续全部为空,直接跳出循环
键值定位:
- 键位置:桶地址 + 数据偏移 + 索引 * 键大小
- 值位置:桶地址 + 数据偏移 + 8*键大小 + 索引 * 值大小
- 间接存储处理:若键或值为指针类型,需解引用获取实际数据
- 未找到处理:返回预定义的零值地址和 false
/*
t *maptype:map 的类型信息
h *hmap:map 的底层结构
key unsafe.Pointer:要查找的键
返回值:
unsafe.Pointer:指向值的指针(未找到时指向零值)
bool:表示键是否存在
*/
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {
if raceenabled && h != nil {
callerpc := getcallerpc()
pc := funcPC(mapaccess2)
racereadpc(unsafe.Pointer(h), callerpc, pc)
raceReadObjectPC(t.key, key, callerpc, pc)
}
if msanenabled && h != nil {
msanread(key, t.key.size)
}
// 如果map为空,或者元素个数为零,直接返回零值
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0)
}
return unsafe.Pointer(&zeroVal[0]), false
}
// 读、写冲突
if h.flags&hashWriting != 0 {
throw("concurrent map read and map write")
}
// 计算哈希值,并且加入 hash0 引入随机性
hash := t.hasher(key, uintptr(h.hash0))
// 如果 B = 3,那么结果用二进制表示就是 111,2^B-1
m := bucketMask(h.B)
// b 就是存储key所在的 bucket 的地址
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))
// h.oldbuckets != nil时,说明 map 发生了扩容。这时候,新的 buckets 里可能还没有老的内容,
// 所以一定要在老的里面找,否则有可能发生“消失”的诡异现象
if c := h.oldbuckets; c != nil {
if !h.sameSizeGrow() {
// 如果不是等量扩容,新bucket的数量是老bucket的两倍
m >>= 1
}
// 求出 key 在老的 map 中的 bucket 位置
oldb := (*bmap)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize)))
// 如果oldb 没有搬迁到新的 bucket,那就在老的 bucket 中寻找
if !evacuated(oldb) {
b = oldb
}
}
// 计算高 8 位的 hash,相当于右移 56 位,只取高8位
top := tophash(hash)
bucketloop:
// 两层循环,第一层遍历bucket后面所有的溢出桶
// 第二层遍历每个桶内部的8个key位置
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
// 如果top不匹配
if b.tophash[i] != top {
// emptyRest标记此cell后面所有的key(包括overflow桶)都为空,此时直接跳出循环
if b.tophash[i] == emptyRest {
break bucketloop
}
continue
}
// 通过 bmap的地址+dataOffset+i*keysize 定位key的位置
dataOffset = unsafe.Offsetof(struct{
b bmap // bmap理解为[bucketCnt]uint8
v int64
}{}.v)
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
if t.indirectkey() { // 如果key是指针,那么解引用
k = *((*unsafe.Pointer)(k)) // 先将k转化为*unsafe.Pointer类型,然后取出该地址存储的值
}
// 如果key相等,定位value的位置,在map中找到了key-value,然后返回
if t.key.equal(key, k) {
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
if t.indirectelem() { // 如果value是指针,那么解引用
e = *((*unsafe.Pointer)(e))
}
return e, true
}
}
}
// 如果遍历完所有的bucket(包括溢出桶),还没找到,则返回零值
return unsafe.Pointer(&zeroVal[0]), false
}3.4 map的扩容
扩容是hash table中比较常见的一种操作,主要用于提高查询效率。Go map的扩容触发条件如下:
是不是已经到了 load factor 的临界点,即元素个数 >= 桶个数*6.5,这时候说明大部分的桶可能都快满了,如果插入新元素,有大概率需要挂在 overflow 的桶上。
overflow 的桶是不是太多了,
当 bucket 总数 < 2 ^ 15 时,overflow 的 bucket 总数 >= bucket 的总数
当 bucket 总数 >= 2 ^ 15 时,overflow 的 bucket >= 2 ^ 15
满足上述两种情况时,就被认为溢出桶太多了。为啥会导致这种情况呢?是因为我们对 map 一边插入,一边删除,会导致其中很多桶出现空洞,这样使得 bucket 使用率不高,值存储得比较稀疏。在查找时效率会下降。
// 装载因子超过 6.5
func overLoadFactor(count int64, B uint8) bool {
return count >= bucketCnt && float32(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}
// overflow buckets 太多
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
if B > 15 {
B = 15
}
// The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
return noverflow >= uint16(1)<<(B&15)
}针对上述两种情况,采用了不同的解决方法:
- 针对 1,将 B + 1,进而 hmap 的 bucket 数组扩容一倍;
- 针对 2,通过移动 bucket 内容,使其倾向于紧密排列从而提高 bucket 利用率(等量扩容)。
3.4.1 hashGrow函数
hashGrow函数只有在往map中新插入一个值,且需要触发扩容时才会被调用。该函数主要根据扩容条件判断需要执行哪一种扩容,然后申请新的bucket内存,更新bucket、oldbucket的信息,具体的迁移工作是由growWork 和 evacuate函数中完成的。Go map的扩容也采用的渐进式迁移,每一次赋值、删除操作时,如果map处于扩容状态,则会保证key对应的索引完全迁移(这样,后续的操作只需要在h.bucket中完成即可),同时将h.nevacuate指示的下标索引对应的所有bucket也完成迁移。
func hashGrow(t *maptype, h *hmap) {
// If we've hit the load factor, get bigger.
// Otherwise, there are too many overflow buckets, so keep the same number of buckets and "grow" laterally.
// 如果 load factor 超过了阈值,那么需要对 map 进行扩容,即 B = B + 1,bucket 总数会变为原来的二倍
// 如果还没到阈值,那么只需要保持相同数量的 bucket,横向拍平就行了(等量扩容)
bigger := uint8(1)
if !overLoadFactor(h.count+1, h.B) {
bigger = 0
h.flags |= sameSizeGrow // 如果flags原先的值小于sameSizeGrow,h.flags += sameSizeGrow
}
// 将原先的bucket分给oldbuckets,然后申请新的bucket内存
oldbuckets := h.buckets
newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
// 先把 h.flags 中 iterator 和 oldIterator 对应位清 0,然后如果发现 iterator 位为 1,那就把它转接到 oldIterator 位,使得 oldIterator 标志位变成 1。
// 潜台词就是:buckets 现在挂到了 oldBuckets 名下了,对应的标志位也转接过去吧。
flags := h.flags &^ (iterator | oldIterator)
if h.flags&iterator != 0 {
flags |= oldIterator
}
// 更新map的信息
h.B += bigger
h.flags = flags
h.oldbuckets = oldbuckets
h.buckets = newbuckets
h.nevacuate = 0
h.noverflow = 0
// 将原先的overflow搬到oldoverflow
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
}
// 如果新的bucket有overflow bucket,赋值给nextOverflow
if nextOverflow != nil {
if h.extra == nil {
h.extra = new(mapextra)
}
h.extra.nextOverflow = nextOverflow
}
// the actual copying of the hash table data is done incrementally by growWork() and evacuate().
// 实际的哈希表元素的拷贝工作是在 growWork 和 evacuate 中增量慢慢地进行的
}3.4.3 evacuate函数
evacuate函数实现迁移的核心点在于:
evacDst结构:跟踪迁移目标位置
等量扩容:只使用 x(相同索引位置,平行迁移)
翻倍扩容:使用 x 和 y 两个目标,将原来一个索引下的bucket内容分配到新bmap数组的两个相关索引下:
x:新桶数组低位(索引 =oldbucket)y:新桶数组高位(索引 =oldbucket + newbit)
在翻倍扩容的时候,虽然键的哈希值没有变化,但通过精妙的迁移策略和新索引计算机制,仍然能确保键的正确查找!翻倍扩容时,新掩码(newbit) = 1 << B(B 是旧桶数量的指数),迁移到新桶数组到高位或地位,决定于hash & newbit。然后在查找时,使用新掩码计算索引,在不改变hash函数的情况完美解决查找问题。(新旧mask的区别就是:新mask比旧值多了 B+1 bit位的判断,后 B bit位还是保持一致,所以可以利用hash的第 B+1位值来确定key迁移到低位还是高位)
新索引 = {
旧索引 (当 hash & newbit == 0 时)
旧索引 + newbit (当 hash & newbit != 0 时)
} ======> 新索引 = hash & (newMask),新mask的第 B+1 bit位的值 == 2^BnewMask = (1 << (B+1)) - 1 = (1 << B) * 2 - 1 = newbit*2 - 1
假设:
- 旧 B=2 (4个桶,掩码 0b11)
- 新 B=3 (8个桶,掩码 0b111)
- newbit = 1<<2 = 4 (0b100)
- 键哈希值:h = 13 (二进制 0b1101)
| 阶段 | 计算过程 | 结果 |
|---|---|---|
| 旧索引 | h & 0b11 = 0b1101 & 0b0011 | 1 (0b01) |
| 迁移决策 | h & newbit = 0b1101 & 0b0100 | 4 (非0) → 迁移到高位 |
| 新索引 | h & 0b111 = 0b1101 & 0b0111 | 5 (0b101) |
| 验证 | 旧索引(1) + newbit(4) | 5 |

// evacDst is an evacuation destination.
type evacDst struct {
b *bmap // current destination bucket
i int // key/elem index into b
k unsafe.Pointer // pointer to current key storage
e unsafe.Pointer // pointer to current elem storage
}
// 该函数用于实现hmap扩容时,数据的搬迁工作
// 如果是等量扩容,那么就初始化一个evacDst
// 如果是翻倍扩容,那么就初始化两个evacDst
func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
// 定位旧bucket的地址以及扩容前map的容量
b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
newbit := h.noldbuckets()
// 如果 b 没有被搬迁过
if !evacuated(b) {
// xy contains the x and y (low and high) evacuation destinations.
// xy 包含的是移动的目标
// 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() {
// Only calculate y pointers if we're growing bigger.
// Otherwise GC can see bad pointers.
// 如果不是等 size 扩容,前后 bucket 序号有变,使用 y 来进行搬迁
// 扩容后的map bucket数量是原先的两倍,x,y分别各占一半,数量与扩容前一样
// y部分桶的编号: oldbucket+newbit
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))
}
// 遍历所有的 bucket,包括 overflow buckets,b 是老的 bucket 地址
for ; b != nil; b = b.overflow(t) {
// 从oldbucket的第一个cell开始
k := add(unsafe.Pointer(b), dataOffset)
e := add(k, bucketCnt*uintptr(t.keysize))
// 遍历 bucket 中的所有 cell
for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
// 当前 cell 的 top hash 值
top := b.tophash[i]
// 如果 cell 为空,即没有 key
if isEmpty(top) {
// 那就标志它被"搬迁"过,继续下一个cell
b.tophash[i] = evacuatedEmpty
continue
}
// 正常不会出现这种情况
// 未被搬迁的 cell 只可能是 empty 或是正常的 top hash(大于 minTopHash)
if top < minTopHash {
throw("bad map state")
}
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
var useY uint8
if !h.sameSizeGrow() {
// Compute hash to make our evacuation decision (whether we need
// to send this key/elem to bucket x or bucket y).
// 计算哈希,以判断我们的数据要转移到哪一部分的 bucket,
// 可能是 x 部分,也可能是 y 部分
hash := t.hasher(k2, uintptr(h.hash0))
if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
// If key != key (NaNs), then the hash could be (and probably
// will be) entirely different from the old hash. Moreover,
// it isn't reproducible. Reproducibility is required in the
// presence of iterators, as our evacuation decision must
// match whatever decision the iterator made.
// Fortunately, we have the freedom to send these keys either
// way. Also, tophash is meaningless for these kinds of keys.
// We let the low bit of tophash drive the evacuation decision.
// We recompute a new random tophash for the next level so
// these keys will get evenly distributed across all buckets
// after multiple grows.
useY = top & 1
top = tophash(hash)
} else {
// 根据hash & newbit 来确定新bucket的索引,确保迁移完成之后,
// 使用原先的hash值 & 新的mask 还能找到正确的索引
if hash&newbit != 0 {
useY = 1
}
}
}
if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
throw("bad evacuatedN")
}
// 在oldbucket对应的cell中标记top的搬迁状态
b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
dst := &xy[useY] // evacuation destination
// 当前bucket中已经放满了元素,需要使用overflow bucket
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.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.
dst.k = add(dst.k, uintptr(t.keysize))
dst.e = add(dst.e, uintptr(t.elemsize))
}
}
// Unlink the overflow buckets & clear key/elem to help GC.
// 如果没有协程在使用老的 buckets,就把老 buckets 清除掉,帮助gc
if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
// Preserve b.tophash because the evacuation state is maintained there.
// 只清除bucket 的 key,value 部分,保留 top hash 部分,指示搬迁状态
ptr := add(b, dataOffset)
n := uintptr(t.bucketsize) - dataOffset
memclrHasPointers(ptr, n)
}
}
// 更新搬迁进度,如果此次搬迁的 bucket 等于当前进度
if oldbucket == h.nevacuate {
advanceEvacuationMark(h, t, newbit)
}
}3.5 map的删除
map的删除主要流程就是需要在key所在的索引bmap链表中查找到对应的值,进行内容的删除释放。这里需要特别注意的是,为了提升查找效率,有一个emptyRest前向传播机制:如果被删除的key后续位置都是emptyRest,则需要将其前面标记为emptyOne的cell升级为emptyRest,表示从这个cell往后不再有数据。

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)
}
// 如果map为空,或者元素个数为零,直接返回
if h == nil || h.count == 0 {
if t.hashMightPanic() {
t.hasher(key, 0)
}
return
}
// 有goroutine在写map时,无法完成删除操作
if h.flags&hashWriting != 0 {
throw("concurrent map writes")
}
// 计算需要写入的key的hash值
hash := t.hasher(key, uintptr(h.hash0))
// 调用hasher函数时,可能发生paninc,因此没法完成一次写操作
// 如果hasher没有发生panic,那么将flags设置成flags += 4
h.flags ^= hashWriting
// 计算低 8 位 hash,根据计算出的 bucketMask 选择对应的 bucket 编号
bucket := hash & bucketMask(h.B)
// 如果map正在扩容,则完成搬迁工作
if h.growing() {
growWork(t, h, bucket)
}
// 计算key对应桶编号的地址,以及hash值的高8位
b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
bOrig := b
top := tophash(hash)
search:
// 两层循环,第一层遍历bucket后面所有的溢出桶
// 第二层遍历每个桶内部的8个key位置
for ; b != nil; b = b.overflow(t) {
for i := uintptr(0); i < bucketCnt; i++ {
// 如果top不匹配
if b.tophash[i] != top {
// emptyRest标记此cell后面所有的key(包括overflow桶)都为空,此时直接跳出循环
if b.tophash[i] == emptyRest {
break search
}
continue
}
// 如果b.tophash[i] == top,计算key对应的地址
// k是指针对象,解引用
k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
// 为了保存key的原始类型,便于后续的清理
k2 := k
if t.indirectkey() {
k2 = *((*unsafe.Pointer)(k2))
}
// 如果相同的 hash 位置的 key 和要插入的 key 字面上不相等
// 如果两个 key 的首八位后最后八位哈希值一样,就会进行其值比较,算是一种哈希碰撞
if !t.key.equal(key, k2) {
continue
}
// Only clear key if there are pointers in it.
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() {
*(*unsafe.Pointer)(e) = nil
} else if t.elem.ptrdata != 0 {
memclrHasPointers(e, t.elem.size)
} else {
memclrNoHeapPointers(e, t.elem.size)
}
// 标记tophash的状态
b.tophash[i] = emptyOne
// 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 { // 删除的key位于bucket的尾巴
// 如果后续还有overflow桶,且桶内部还有元素,那么直接跳到notLast
// 将此tophash标记为emptyOne即可
if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
goto notLast
}
} else {
// 如果key不是最后一个,且后续cell还有元素,也直接跳到notLast
if b.tophash[i+1] != emptyRest {
goto notLast
}
}
for {
// 删除的key后面不再有元素,需要将tophash标记为emptyRest
// 同时往前寻找,并将前面tophash为emptyOne的位置标记为emptyRest
b.tophash[i] = emptyRest
if i == 0 {
if b == bOrig {
break // beginning of initial bucket, we're done.
}
// Find previous bucket, continue at its last entry.
c := b
// 从key的bucket开始,查找当前bucket的前一个桶地址
for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
}
// bucket内部从最后一个cell开始查找tophash为emptyOne的cell
i = bucketCnt - 1
} else {
i--
}
// 查找到一个有内容的tophash,结束循环
if b.tophash[i] != emptyOne {
break
}
}
notLast:
h.count--
// Reset the hash seed to make it more difficult for attackers to
// repeatedly trigger hash collisions. See issue 25237.
if h.count == 0 {
h.hash0 = fastrand()
}
break search
}
}
// 禁止并发写
if h.flags&hashWriting == 0 {
throw("concurrent map writes")
}
// flags对hashWriting按位置0,
// '^='表示按右边hashWriting二进制为1的位置,置0,其他位置与flags保持一致
// 还原写操作之前的flags
h.flags &^= hashWriting
}3.6 map的遍历
3.6.1 hiter结构体
在对map进行遍历时,会借助迭代器hiter辅助完成整个map的循环遍历,其中startBucket标记此次遍历的起始位置,wrapped标记遍历是否从头开始,checkBucket则是用于扩容中进行遍历时,坚持oldBucket中的数据。
type hiter struct {
// key 指针,保存此次遍历得到的key
key unsafe.Pointer
// value 指针,保存此次遍历得到的value
value unsafe.Pointer
// map 类型,包含如 key size 大小等
t *maptype
// map header
h *hmap
// 初始化时指向的 bucket
buckets unsafe.Pointer
// 当前遍历到的 bmap
bptr *bmap
overflow [2]*[]*bmap
// 起始遍历的 bucet 编号
startBucket uintptr
// 遍历开始时 cell 的编号(每个 bucket 中有 8 个 cell)
offset uint8
// 是否从头遍历了
wrapped bool
// B 的大小
B uint8
// 指示当前 cell 序号
i uint8
// 指向当前的 bucket
bucket uintptr
// 因为扩容,需要检查的 bucket
checkBucket uintptr
}3.6.2 mapiternext函数
对map进行遍历时,首先会调用mapiterinit函数,初始化hiter,该函数逻辑比较简单,主要就是随机确定遍历起始位置,这里的起始位置包括:bmap的数组的起始下标、bmap bucket内部cell的起始点。随后调用mapiternext函数开始执行具体的遍历操作(每调用一次mapiternext函数获取一个map中的键值对),该过程的主要流程如下:
1. 首先检查是否有并发写操作,如果有则抛出异常。
2. 获取迭代器当前的状态(当前遍历的bucket,当前bucket内的位置i,当前bucket指针b等)。
3. 如果当前bucket(b)为nil,说明需要处理下一个bucket或者已经遍历完一圈。
- 如果当前bucket序号(bucket)等于起始bucket(it.startBucket)且已经标记为绕回(wrapped),说明已经遍历完所有bucket,设置key和elem为nil并返回。
- 如果map正在扩容且迭代器开始时的B(it.B)等于当前map的B(h.B),说明迭代开始后扩容未完成,需要处理旧桶:
- 计算对应的旧桶(oldbucket)地址。
- 如果该旧桶尚未迁移(evacuated),则设置checkBucket为当前bucket(用于后续判断键是否属于当前新桶),并继续遍历旧桶。
- 如果已经迁移,则直接遍历新桶中对应位置的桶,并将checkBucket设置为noCheck。
- 否则,直接遍历新桶中当前bucket位置的桶。
- 然后bucket序号加1,如果达到bucket总数(2^B),则重置为0并标记wrapped为true(表示已经绕回一圈)。
- 重置i为0,开始遍历新桶。
4. 遍历当前bucket(b)的8个cell(槽位),注意这里不是从0开始,而是根据it.offset进行偏移(使用offi = (i+it.offset) & (bucketCnt-1))以实现随机开始。
- 如果tophash[offi]为空(emptyOne或evacuatedEmpty)则跳过。
- 获取键k和值e的地址。
- 如果checkBucket不为noCheck(说明在扩容过程中且当前遍历的是旧桶),则需要判断该键是否属于当前迭代的新桶(checkBucket):
- 对于正常键(非NaN),计算其哈希值并判断它是否属于checkBucket,如果不属于则跳过。
- 对于NaN(k!=k),使用tophash的低位来决定它应该去哪个桶,如果与checkBucket的高位不匹配则跳过。
- 如果键值有效,则判断该槽位是否已经被迁移(evacuatedX或evacuatedY)且键是正常的(可比较且相等):
- 如果未被迁移,或者键是NaN(不可比较),则直接使用当前找到的键值对(因为此时数据还未迁移或无法比较,所以认为有效)。
- 否则,说明在迭代过程中map已经扩容完成,该键可能已经被迁移,需要从新map中查找(调用mapaccessK):如果返回的键为nil,说明该键已被删除,跳过;否则使用返回的键值对。
5. 设置迭代器的状态(key, elem, bucket, bptr, i, checkBucket)并返回。
6. 如果当前bucket的8个cell都遍历完了,则跳到overflow bucket(如果有),重置i为0,然后跳转到next标签继续处理。
func mapiternext(it *hiter) {
h := it.h
if raceenabled {
callerpc := getcallerpc()
racewritepc(unsafe.Pointer(h), callerpc, funcPC(mapiternext))
}
// 如果有goroutine正在写,抛出异常
if h.flags&hashWriting != 0 {
throw("concurrent map iteration and writes")
}
t := it.t
bucket := it.bucket
b := it.bptr
i := it.i
checkBucket := it.checkBucket
next:
// 后续没有overflow bucket需要遍历
if b == nil {
// 当前遍历的bucket=startBucket,即完成循环回到最初的位置
// 且该bucket已经从头到尾遍历完了,map的遍历结束
if bucket == it.startBucket && it.wrapped {
// end of iteration
it.key = nil
it.elem = nil
return
}
// 如果map正处于扩容状态,还需要遍历oldbucket
if h.growing() && it.B == h.B {
// Iterator was started in the middle of a grow, and the grow isn't done yet.
// If the bucket we're looking at hasn't been filled in yet (i.e. the old
// bucket hasn't been evacuated) then we need to iterate through the old
// bucket and only return the ones that will be migrated to this bucket.
oldbucket := bucket & it.h.oldbucketmask()
// 根据bucket num在oldbucket的编号位置,计算bucket的地址
b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
if !evacuated(b) {
// 如果oldbucket中此编号的bucket没有完成搬迁,那么标记check
checkBucket = bucket
} else {
// 如果oldbucket中此编号的bucket完成了搬迁,直接遍历newbucket中对应位置的bucket
b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
checkBucket = noCheck
}
} else {
b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
checkBucket = noCheck
}
// 指向下一个bucket
bucket++
// 判断是否已经遍历到了末尾,如果是,则需要从头重新开始遍历,
// 直至bucket == startbucket,标记一次完整的遍历
if bucket == bucketShift(it.B) {
bucket = 0
it.wrapped = true
}
i = 0
}
// 遍历bucket的8个cell,但不是从头开始遍历的
for ; i < bucketCnt; i++ {
offi := (i + it.offset) & (bucketCnt - 1)
// 如果tophash为空,即没有数据,直接检查下一个
if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {
// TODO: emptyRest is hard to use here, as we start iterating
// in the middle of a bucket. It's feasible, just tricky.
continue
}
k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
if t.indirectkey() {
k = *((*unsafe.Pointer)(k))
}
e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
if checkBucket != noCheck && !h.sameSizeGrow() {
// Special case: iterator was started during a grow to a larger size
// and the grow is not done yet. We're working on a bucket whose
// oldbucket has not been evacuated yet. Or at least, it wasn't
// evacuated when we started the bucket. So we're iterating
// through the oldbucket, skipping any keys that will go
// to the other new bucket (each oldbucket expands to two buckets during a grow).
if t.reflexivekey() || t.key.equal(k, k) {
// If the item in the oldbucket is not destined for the current new bucket in the iteration, skip it.
hash := t.hasher(k, uintptr(h.hash0))
if hash&bucketMask(it.B) != checkBucket {
continue
}
} else {
// Hash isn't repeatable if k != k (NaNs). We need a
// repeatable and randomish choice of which direction
// to send NaNs during evacuation. We'll use the low
// bit of tophash to decide which way NaNs go.
// NOTE: this case is why we need two evacuate tophash
// values, evacuatedX and evacuatedY, that differ in their low bit.
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)) {
// This is the golden data, we can return it.
// OR
// key!=key, so the entry can't be deleted or updated, so we can just return it.
// That's lucky for us because when key!=key we can't look it up successfully.
it.key = k
if t.indirectelem() {
e = *((*unsafe.Pointer)(e))
}
it.elem = e
} else {
// The hash table has grown since the iterator was started.
// The golden data for this key is now somewhere else.
// Check the current hash table for the data.
// This code handles the case where the key has been deleted, updated, or deleted and reinserted.
// NOTE: we need to regrab the key as it has potentially been
// updated to an equal() but not identical key (e.g. +0.0 vs -0.0).
rk, re := mapaccessK(t, h, k)
if rk == nil {
continue // key has been deleted
}
it.key = rk
it.elem = re
}
it.bucket = bucket
if it.bptr != b { // avoid unnecessary write barrier; see issue 14921
it.bptr = b
}
it.i = i + 1
it.checkBucket = checkBucket
return
}
// 通过it.i走到此处,遍历overflow bucket
b = b.overflow(t)
i = 0
goto next
}总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。
