设为首页收藏本站
网站公告 | 这是第一条公告
     

 找回密码
 立即注册
缓存时间23 现在时间23 缓存数据 荣耀也罢,屈辱也罢,都要以平和的心态去面对,少一些无奈与感慨,多一份从容和淡然。晚安!

荣耀也罢,屈辱也罢,都要以平和的心态去面对,少一些无奈与感慨,多一份从容和淡然。晚安!

查看: 123|回复: 1

Go数据结构之映射map方式

[复制链接]

  离线 

TA的专栏

  • 打卡等级:热心大叔
  • 打卡总天数:231
  • 打卡月天数:1
  • 打卡总奖励:4056
  • 最近打卡:2025-12-09 08:47:46
等级头衔

等級:晓枫资讯-上等兵

在线时间
5 小时

积分成就
威望
0
贡献
455
主题
391
精华
0
金钱
5324
积分
906
注册时间
2023-1-3
最后登录
2025-12-9

发表于 2025-9-8 07:43:07 | 显示全部楼层 |阅读模式
Map作为Go原生支持的另一个主要的数据结构,其使用频繁程度与切片有过之而无不及。用一句话描述map:存储键-值对映射关系的无序数据结构,保证键的唯一性但不保证并发操作安全。
本文将介绍map的基本使用以及go 1.16版本其底层实现。

1 map的使用


1.1 map基本使用

声明/初始化,一般有如下三种方式,

  • 使用var声明一个map变量,需要指定键值对分别的类型,这种方式仅仅只是声明,返回的是一个nil map,既没有分配内存,无法对其进行写入操作。如果要对其进行写入操作,则还需要为其分配内存。
  • 比较常用的是直接使用make初始化,在初始化map类型时,可以传一个容量(通常是不传这个参数,因为map底层有自动扩容机制,且无法准确预估map所需的容量)。
  • 第三种方式就是直接在定义的同时,对map进行写入操作,这种方式适用于确定部分需要写入map的场景
  1. // 声明一个map变量,键是string类型,值是int类型
  2. // 但此时m还是一个nil map,无法对其进行写入
  3. var m map[string]int
  4. m["1"] = 1 // 报错,nil map无法写入

  5. m = make(map[string]int) // 使用make为map分配内存,此时可以正常写入


  6. // 可以将声明和分配内存合二为一,直接使用make函数为map分配内存
  7. // make函数第一个参数是map的类型,第二个参数可选,表示map的容量大小
  8. m1 := make(map[string]int)

  9. // 第三种方式就是在定义的时候,连带着给map进行赋值
  10. m2 := map[string]int{
  11.     "1": 1,
  12.     "2": 2,
  13. }
复制代码
赋值,其实就比较简单,直接将键放入中括号,对应的值放在等号右边即可,如果键之间存在,则实现更新;不存在,则实现插入。这里需要特别注意的是

  • 对于nil map无法进行赋值操作,如果对nil map进行赋值,则会panic。
  • map中的value是不可寻址的,无法直接通过map值进行更新操作,但有两种例外,1)value为int类型,编译器会通过语法糖进行直接更新;2)value为指针类型时,也能直接通过map值进行更新操作。
  1. m := make(map[string]int)
  2. m["1"] = 1

  3. type Person struct {
  4.         name string
  5.         age  int
  6. }

  7. m1 := map[string]Person{}
  8. m1["Tom"].age++ // 错误,因为map的值是无法寻址的,这种情况需要接受旧值,将修改完后的值重新赋值

  9. m2 := map[string]*Person{}
  10. m2["Tom"].age++ // 正确,这种情况下没有改变map中的value,而是通过指针找到对应存储的值进行修改
复制代码

  • 查找,map对于查询的处理原则就是,如果key不存在,则返回value对应的零值(nil map也能进行查找,只是对于所有的查询都返回value类型零值)。如果需要判断key是否存在,则推荐使用v, ok := m["1"]的写法,ok为true表示key存在于map中。
  1. var m map[string]int
  2. fmt.Println(m["1"]) // nil map可以进行查找,返回的是value的默认零值

  3. // 如果需要判断key是否存在于map中,则可以使用如下写法
  4. if _, ok := m["1"]; ok {
  5.     fmt.Println("key存在")
  6. } else {
  7.     fmt.Println("key不存在")
  8. }
复制代码

  • 删除,主要通过调用delete函数实现map中键值对的删除操作,对于nil map也能执行删除操作,如果待删除的key不存在,则不做任何处理。
  1. var m map[string]int
  2. delete(m ,"1")
复制代码

  • 遍历,只能通过for range进行map的遍历操作,而且遍历是无序的,每次遍历结果都不一样,这样做:一是为了让使用者不依赖于map的遍历顺序,二也是与map的底层实现有很大关系。实际开发过程中,如果要确定的遍历顺序,往往需要借助切片保存顺序,然后通过遍历切片去map中取值。
  1. m := map[string]int{
  2.     "1": 1,
  3.     "2": 2,
  4. }

  5. // 无序遍历,每次遍历的顺序都不一样
  6. for k, v := range m {
  7.     fmt.Println(k, v)
  8. }
复制代码
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,例如:
  1. m := map[string]int{"test":1}

  2. m["test"]++  // 实际上是语法糖
  3. /*
  4. temp := m["test"]
  5. temp += 1
  6. m["test"] = temp
  7. */
复制代码
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的抽象,为提高存储效率,每一个
  1. bmap
复制代码
都能存储 8 个键值对。
当哈希表中存储的数据过多,单个桶已经装满时就会使用
  1. extra.nextOverflow
复制代码
中桶存储溢出的数据。上述两种不同的桶在内存中是连续存储的,我们在这里将它们分别称为正常桶和溢出桶。
  1. bucket
复制代码
的结构体
  1. bmap
复制代码
在 Go 语言源代码中的定义只包含一个简单的
  1. tophash
复制代码
字段,
  1. tophash
复制代码
存储了键的哈希的高 8 位,通过比较不同键的哈希的高 8 位可以减少访问键值对次数以提高性能。在运行期间,
  1. bmap
复制代码
结构体其实不止包含
  1. tophash
复制代码
字段,因为哈希表中可能存储不同类型的键值对,而且 Go 语言也不支持泛型,所以键值对占据的内存空间大小只能在编译时进行推导。
  1. runtime.bmap
复制代码
中的其他字段在运行时也都是通过计算内存地址的方式访问的,所以它的定义中就不包含这些字段,不过我们能根据编译期间的
  1. cmd/compile/internal/gc.bmap
复制代码
函数重建它的结构:
  1. type bmap struct {
  2.     tophash [bucketCnt]uint8
  3. }

  4. // 利用../go/src/cmd/compile/internal/gc/reflect.go文件中的bmap函数反推
  5. type bmap struct {
  6.     topbits  [8]uint8
  7.     keys     [8]keytype
  8.     values   [8]valuetype
  9.     overflow uintptr
  10. }
复制代码
1.jpeg

桶和溢出桶

  • 桶(bucket): 每个桶包含固定数量的键值对,Go 中每个桶默认可以存储 8 对键值对。
  • 溢出桶(overflow bucket): 当一个桶已满但仍需插入新的键值对时,会创建一个新的桶作为当前桶的溢出桶。
溢出桶的作用
解决哈希冲突:

  • 在理想情况下,每个键会被分配到不同的桶中。然而,在实际应用中,由于哈希函数的特性,多个键可能会被分配到同一个桶中(即哈希冲突)。
  • 当一个桶已满且仍然需要存储新的键值对时,Go 会创建一个新的桶作为当前桶的溢出桶,并将新键值对存储在这个溢出桶中。
管理存储空间:

  • 溢出桶允许动态扩展 map 的存储容量。通过使用链表结构(由多个溢出桶组成),可以有效地管理超过初始桶容量的数据。
  • 这种机制避免了频繁地重新分配和复制整个哈希表,从而提高了性能。
高效查找:

  • 即使存在多个溢出桶,Go 的
    1. map
    复制代码
    实现仍然能够高效地进行键值对的查找。通过遍历主桶及其关联的溢出桶,可以快速找到所需的键值对。
  • 溢出桶的设计确保了查找操作在平均情况下仍然是 O(1) 时间复杂度。

2.2 hmap

hmap是Go实现map的底层数据结构,主要用于管理bucket的扩容、元素的查找/删除等,其具体结构如下:
  1. type hmap struct {
  2.    count       int        // 元素个数,调用 len(map) 时,直接返回此值
  3.    flags       uint8      // 标记,对应 const 中 '// flags' 下的几个状态
  4.    B           uint8      // buckets 的对数 log_2
  5.    noverflow   uint16     // overflow 的 bucket 近似数
  6.    hash0       uint32     // 计算 key 的哈希的时候会传入哈希函数
  7.    buckets     unsafe.Pointer    // 指向 buckets 数组,大小为 2^B。如果元素个数为0,就为 nil
  8.                                  // 扩容的时候,buckets 长度会是 oldbuckets 的两倍
  9.    oldbuckets unsafe.Pointer     // 指向扩容前的数组(渐进式扩容)
  10.    nevacuate  uintptr            // 指示扩容进度,小于此地址的 buckets 迁移完成
  11.    extra      *mapextra          //保存溢出桶的信息
  12. }


  13. type mapextra struct {
  14.    // 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节)
  15.    // 使用 extra 来存储 overflow bucket,这样可以避免 GC 扫描整个 map
  16.    // 然而 bmap.overflow 也是个指针。这时候我们只能把这些 overflow 的指针
  17.    // 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了
  18.    overflow    *[]*bmap // overflow 包含的是 hmap.buckets 的 overflow 的 bucket
  19.    oldoverflow *[]*bmap // oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow 的 bucket

  20.    nextOverflow *bmap // 指向空闲的 overflow bucket 的指针
  21. }
复制代码
2.jpeg


3 map实现源码


3.1 map的初始化

使用make函数创建map:
  1. 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 结构体:若传入
    1. h
    复制代码
    为 nil,新建
    1. hmap
    复制代码
    结构(map 的底层表示);随后
    1. h.hash0 = fastrand()
    复制代码
    :生成随机哈希种子,用于增加哈希随机性,防止哈希碰撞攻击,相同 key 在不同 map 中产生不同哈希值
  • 计算桶数量指数 B:根据负载因子确定合适的桶数量(
    1. 2^B
    复制代码
    个桶),循环增加
    1. B
    复制代码
    直到满足:
    1. 6.5 * 2^B >= hint
    复制代码
  • 分配桶数组:若
    1. B == 0
    复制代码
    (hint=0),延后到首次写入时分配;其他情况,调用
    1. makeBucketArray
    复制代码
    分配主桶数组(长度为
    1. 2^B
    复制代码
    ),如果 B >= 4 额外预分配溢出桶(减少运行时分配开销,这里也说明正常桶与溢出桶是内存地址连续的数组)
  • 溢出桶管理:若存在预分配溢出桶(
    1. nextOverflow != nil
    复制代码
    ),初始化
    1. h.extra
    复制代码
    结构,记录可用溢出桶链表。最后一个溢出桶的overflow指针会指向第一个正常桶,以此形成一个环。
  1. func makemap_small() *hmap {
  2.    h := new(hmap)
  3.    h.hash0 = fastrand()
  4.    return h
  5. }

  6. func makemap(t *maptype, hint int, h *hmap) *hmap {
  7.    // 进行内存大小的检查,如果溢出或者内存超出最大内存空间,将hint(元素个数)设置为0
  8.    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
  9.    if overflow || mem > maxAlloc {
  10.       hint = 0
  11.    }

  12.    // 初始化 Hmap,即如果当前Hamp为空,则new hmap
  13.    // 设置map的哈希计算种子随机数hash0
  14.    if h == nil {
  15.       h = new(hmap)
  16.    }
  17.    h.hash0 = fastrand()

  18.    // Find the size parameter B which will hold the requested # of elements.
  19.    // For hint < 0 overLoadFactor returns false since hint < bucketCnt.
  20.    // 按照提供的元素个数,根据负载因子计算合理的 B 值,以确保 6.5 * 2 ^ B >= hint
  21.    B := uint8(0)
  22.    for overLoadFactor(hint, B) {
  23.       B++
  24.    }
  25.    h.B = B

  26.    // allocate initial hash table
  27.    // if B == 0, the buckets field is allocated lazily later (in mapassign)
  28.    // If hint is large zeroing this memory could take a while.
  29.    // 初始化 hash table
  30.    // 如果 B 等于 0,那么 buckets 就会在赋值( mapassign )的时候再分配
  31.    // 如果长度比较大,分配内存会花费长一点
  32.    if h.B != 0 {
  33.       var nextOverflow *bmap
  34.       h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)
  35.       if nextOverflow != nil {
  36.          h.extra = new(mapextra)
  37.          h.extra.nextOverflow = nextOverflow
  38.       }
  39.    }

  40.    return h
  41. }

  42. func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
  43.    // uintptr(1) << ( b & (sys.PtrSize*8-1))  2^b
  44.    base := bucketShift(b)
  45.    nbuckets := base

  46.    // 当桶的数量小于2^4 时,由于数据较少、使用溢出桶的可能性较低,
  47.    // 这时就会省略创建溢出桶的过程以减少额外开销
  48.    if b >= 4 {
  49.       // 当桶的数量多于2^4时,就会额外创建 2^(b-4)个溢出桶
  50.       // 根据内存的分配规则,计算出合适的内存大小,并确定桶的数量
  51.       nbuckets += bucketShift(b - 4)
  52.       sz := t.bucket.size * nbuckets
  53.       up := roundupsize(sz)
  54.       if up != sz {
  55.          nbuckets = up / t.bucket.size
  56.       }
  57.    }

  58.    // 如果桶之前没有分配内存,就初始化一个数组
  59.    // 反之,直接沿用之前的,并清理掉本次初始化所需要内存大小的内存,备用
  60.    if dirtyalloc == nil {
  61.       buckets = newarray(t.bucket, int(nbuckets))
  62.    } else {
  63.       // dirtyalloc was previously generated by
  64.       // the above newarray(t.bucket, int(nbuckets))
  65.       // but may not be empty.
  66.       buckets = dirtyalloc
  67.       size := t.bucket.size * nbuckets
  68.       if t.bucket.ptrdata != 0 {
  69.          memclrHasPointers(buckets, size)
  70.       } else {
  71.          memclrNoHeapPointers(buckets, size)
  72.       }
  73.    }

  74.    // 如果创建的桶数量不等于2^b,说明分配了额外的溢出桶
  75.    if base != nbuckets {
  76.       // 2^b个桶后面就是溢出桶
  77.       nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))

  78.       // 取nextOverflow 里面的最后一个元素,并把最后一个buckets 的末尾偏移的指针指向空闲的bucket (目前就是第一个buckets了)
  79.       last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
  80.       last.setoverflow(t, (*bmap)(buckets))
  81.    }
  82.    return buckets, nextOverflow
  83. }
复制代码
下图是不同B值时,初始化得到的map,如果B小于4,则编译器不会为map分配溢出桶(认为map的规模比较小,需要使用到溢出桶的概率不大);只有在B大于等于4时,才会为map分配 2^(B-4)个溢出桶,需要注意的是正常桶与溢出桶在底层是一个内存地址连续的数组!
3.jpeg


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存储的下标位置。
4.jpeg

赋值操作底层mapassign函数的主要流程:
1. 安全检查和初始化

  • 空 map 检查:对 nil map 赋值会 panic
  • 并发写检查:检测到其他 goroutine 正在写 map 时抛出异常(Go map 非并发安全)
  • 哈希计算:使用类型特定的哈希函数计算键的哈希值
  • 标记写操作:设置
    1. hashWriting
    复制代码
    标志位(防止并发写)
  • 延迟初始化:若桶数组未分配,分配一个桶(最小化空 map 开销)
2. 定位桶和搜索键
双层循环搜索:外层遍历对应index下所有的bucket,内层循环处理每个桶的 8 个槽。

  • 遍历时,需要记录第一个可以插入的位置。
  • 同时,需要遍历完全插入位置下,所有的bucket。如果遇到tophash相等,进一步判断key是否一致,key也相等,则更新key的信息并结束循环。
  • 遇到
    1. emptyRest
    复制代码
    (后续全空)时,提前终止搜索。
5.jpeg

3. 键不存在时的处理

  • 扩容条件:负载因子超标(
    1. count/(2^B) > 6.5
    复制代码
    );溢出桶过多(
    1. noverflow >= 2^min(B, 15)
    复制代码
    )。
  • 扩容后重试:桶布局改变需重新定位
  • 分配新溢出桶:当所有桶都无空闲槽时,则申请一个溢出桶
4. 收尾工作

  • 返回值的存储位置:调用方通过此指针写入值
  • 间接值处理:若值类型为指针,返回实际值地址
  1. /*
  2. t *maptype:map 的类型信息
  3. h *hmap:map 的底层结构
  4. key unsafe.Pointer:要插入或更新的键
  5. 返回:指向值存储位置的指针(写入值需通过此指针)
  6. */
  7. func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
  8.    // 对于nil的map不能进行写操作,直接panic
  9.    if h == nil {
  10.       panic(plainError("assignment to entry in nil map"))
  11.    }
  12.    if raceenabled {
  13.       callerpc := getcallerpc()
  14.       pc := funcPC(mapassign)
  15.       racewritepc(unsafe.Pointer(h), callerpc, pc)
  16.       raceReadObjectPC(t.key, key, callerpc, pc)
  17.    }
  18.    if msanenabled {
  19.       msanread(key, t.key.size)
  20.    }

  21.    // 如果有别的goroutine正在写此map,即发生了并发写,直接异常退出
  22.    if h.flags&hashWriting != 0 {
  23.       throw("concurrent map writes")
  24.    }
  25.    // 计算需要写入的key的hash值
  26.    hash := t.hasher(key, uintptr(h.hash0))

  27.    // 调用hasher函数时,可能发生paninc,因此没法完成一次写操作
  28.    // 如果hasher没有发生panic,那么将flags设置成flags += 4
  29.    h.flags ^= hashWriting

  30.    // 如果bucket没有被分配内存,则分配一个bucket(延迟初始化)
  31.    if h.buckets == nil {
  32.       h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
  33.    }

  34. again:
  35.    //  计算低 8 位 hash,根据计算出的 bucketMask 选择对应的 bucket 编号
  36.    bucket := hash & bucketMask(h.B)
  37.    //  如果map正在扩容,则完成搬迁工作
  38.    if h.growing() {
  39.       growWork(t, h, bucket)
  40.    }
  41.    // 计算key对应桶编号的地址,以及hash值的高8位
  42.    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
  43.    top := tophash(hash)

  44.    var inserti *uint8            //  需要写入tophash数组的下标
  45.    var insertk unsafe.Pointer    //  写入key的对象指针(地址)
  46.    var elem unsafe.Pointer       //  写入value的对象指针(地址),即返回值
  47. bucketloop:
  48.    //  开启双层循环,外层遍历bucket的所有overflow桶,内层遍历每个bucket的cell
  49.    //  目的:找到空的cell(key不存在),或者top所在的位置(key已存在)
  50.    for {
  51.       for i := uintptr(0); i < bucketCnt; i++ {
  52.          if b.tophash[i] != top { // 当前top不一致,继续比对下一个
  53.             // 找到第一个空的cell,并保存下表以及地址信息
  54.             if isEmpty(b.tophash[i]) && inserti == nil {
  55.                inserti = &b.tophash[i]
  56.                insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
  57.                elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
  58.             }
  59.             // 此cell为空且后面也都是空cell,那么inserti必定已不为空。
  60.             // 这种情况直接退出bucket cell的遍历
  61.             if b.tophash[i] == emptyRest {
  62.                break bucketloop
  63.             }
  64.             continue
  65.          }

  66.          // 如果b.tophash[i] == top,计算key对应的地址
  67.          // k是指针对象,解引用
  68.          k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
  69.          if t.indirectkey() {
  70.             k = *((*unsafe.Pointer)(k))
  71.          }

  72.          // 如果相同的 hash 位置的 key 和要插入的 key 字面上不相等
  73.          // 如果两个 key 的首八位后最后八位哈希值一样,就会进行其值比较,算是一种哈希碰撞吧
  74.          if !t.key.equal(key, k) {
  75.             continue
  76.          }
  77.          // map已经有此key存在了,那么直接更新
  78.          if t.needkeyupdate() {
  79.             typedmemmove(t.key, k, key)
  80.          }
  81.          elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
  82.          goto done
  83.       }

  84.       //  bucket 的 8 个槽没有满足条件的能插入或者能更新的,去 overflow 里继续找
  85.       //  overflow 为 nil,说明到了 overflow 链表的末端了,退出外层循环
  86.       ovf := b.overflow(t)
  87.       if ovf == nil {
  88.          break
  89.       }
  90.       // 赋值为链表的下一个元素,继续循环
  91.       b = ovf
  92.    }

  93.    // 没有找到 key,分配新的空间
  94.    // 如果触发了最大的 load factor,或者已经有太多 overflow buckets
  95.    // 并且这个时刻没有在进行 growing 的途中,那么就开始 growing
  96.    if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
  97.       hashGrow(t, h)
  98.       goto again // Growing the table invalidates everything, so try again
  99.    }

  100.    if inserti == nil {
  101.       // The current bucket and all the overflow buckets connected to it are full, allocate a new one.
  102.       // 前面在桶里找的时候,没有找到能塞这个 tophash 的位置
  103.       // 说明当前所有 buckets 都是满的,分配一个新的 bucket
  104.       newb := h.newoverflow(t, b)
  105.       inserti = &newb.tophash[0]
  106.       insertk = add(unsafe.Pointer(newb), dataOffset)
  107.       elem = add(insertk, bucketCnt*uintptr(t.keysize))
  108.    }

  109.    // store new key/elem at insert position
  110.    if t.indirectkey() { // 如果键的比较大,则直接使用指针存储
  111.       kmem := newobject(t.key)
  112.       // 二级指针,insertk看作是指向指针的指针
  113.       *(*unsafe.Pointer)(insertk) = kmem
  114.       insertk = kmem
  115.    }
  116.    if t.indirectelem() {
  117.       vmem := newobject(t.elem)
  118.       *(*unsafe.Pointer)(elem) = vmem
  119.    }
  120.    typedmemmove(t.key, insertk, key)
  121.    *inserti = top
  122.    h.count++

  123. done:
  124.    // 禁止并发写
  125.    if h.flags&hashWriting == 0 {
  126.       throw("concurrent map writes")
  127.    }
  128.    // flags对hashWriting按位置0,'^='表示按右边hashWriting二进制为1的位置,置0
  129.    // 还原写操作之前的flags
  130.    h.flags &^= hashWriting
  131.    if t.indirectelem() {
  132.       // 如果value是个大对象,则bucket中存储的是对象地址unsafe.Pointer,取出存放value对象的地址
  133.       elem = *((*unsafe.Pointer)(elem))
  134.    }
  135.    return elem
  136. }
复制代码
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 非并发安全)
计算哈希和定位桶

    1. bucketMask(h.B)
    复制代码
    :生成用于取模的掩码(如 B=3 时,mask=0b111)
  • 桶定位公式:桶地址 = buckets起始地址 + (hash & mask) * 桶大小
扩容期间的特殊处理:当 map 正在扩容时,数据可能存在于新旧桶中(扩容有等量扩容与双倍扩容两种,详细解释见3.4节),如果是双倍扩容,且该下标还没有迁移完成,则在老桶中查找
双层循环搜索键值对

  • tophash 比较:先比较哈希高8位,快速过滤不匹配项
  • emptyRest 优化:遇到标记为
    1. emptyRest
    复制代码
    的槽位,表示后续全部为空,直接跳出循环
键值定位

  • 键位置:桶地址 + 数据偏移 + 索引 * 键大小
  • 值位置:桶地址 + 数据偏移 + 8*键大小 + 索引 * 值大小
  • 间接存储处理:若键或值为指针类型,需解引用获取实际数据
  • 未找到处理:返回预定义的零值地址和 false
  1. /*
  2.     t *maptype:map 的类型信息
  3.     h *hmap:map 的底层结构
  4.     key unsafe.Pointer:要查找的键
  5.     返回值:
  6.         unsafe.Pointer:指向值的指针(未找到时指向零值)
  7.         bool:表示键是否存在
  8. */
  9. func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool) {
  10.    if raceenabled && h != nil {
  11.       callerpc := getcallerpc()
  12.       pc := funcPC(mapaccess2)
  13.       racereadpc(unsafe.Pointer(h), callerpc, pc)
  14.       raceReadObjectPC(t.key, key, callerpc, pc)
  15.    }
  16.    if msanenabled && h != nil {
  17.       msanread(key, t.key.size)
  18.    }

  19.    // 如果map为空,或者元素个数为零,直接返回零值
  20.    if h == nil || h.count == 0 {
  21.       if t.hashMightPanic() {
  22.          t.hasher(key, 0)
  23.       }
  24.       return unsafe.Pointer(&zeroVal[0]), false
  25.    }

  26.    // 读、写冲突
  27.    if h.flags&hashWriting != 0 {
  28.       throw("concurrent map read and map write")
  29.    }

  30.    // 计算哈希值,并且加入 hash0 引入随机性
  31.    hash := t.hasher(key, uintptr(h.hash0))
  32.    // 如果 B = 3,那么结果用二进制表示就是 111,2^B-1
  33.    m := bucketMask(h.B)
  34.    // b 就是存储key所在的 bucket 的地址
  35.    b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))

  36.    // h.oldbuckets != nil时,说明 map 发生了扩容。这时候,新的 buckets 里可能还没有老的内容,
  37.    // 所以一定要在老的里面找,否则有可能发生“消失”的诡异现象
  38.    if c := h.oldbuckets; c != nil {
  39.       if !h.sameSizeGrow() {
  40.          // 如果不是等量扩容,新bucket的数量是老bucket的两倍
  41.          m >>= 1
  42.       }
  43.       // 求出 key 在老的 map 中的 bucket 位置
  44.       oldb := (*bmap)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize)))
  45.       // 如果oldb 没有搬迁到新的 bucket,那就在老的 bucket 中寻找
  46.       if !evacuated(oldb) {
  47.          b = oldb
  48.       }
  49.    }
  50.    // 计算高 8 位的 hash,相当于右移 56 位,只取高8位
  51.    top := tophash(hash)
  52. bucketloop:
  53.    // 两层循环,第一层遍历bucket后面所有的溢出桶
  54.    //         第二层遍历每个桶内部的8个key位置               
  55.    for ; b != nil; b = b.overflow(t) {
  56.       for i := uintptr(0); i < bucketCnt; i++ {
  57.          // 如果top不匹配
  58.          if b.tophash[i] != top {
  59.             // emptyRest标记此cell后面所有的key(包括overflow桶)都为空,此时直接跳出循环
  60.             if b.tophash[i] == emptyRest {
  61.                break bucketloop
  62.             }
  63.             continue
  64.          }

  65.          // 通过 bmap的地址+dataOffset+i*keysize 定位key的位置
  66.          dataOffset = unsafe.Offsetof(struct{
  67.             b bmap // bmap理解为[bucketCnt]uint8
  68.             v int64
  69.          }{}.v)
  70.          
  71.          k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
  72.          if t.indirectkey() {  // 如果key是指针,那么解引用
  73.             k = *((*unsafe.Pointer)(k)) // 先将k转化为*unsafe.Pointer类型,然后取出该地址存储的值
  74.          }
  75.          // 如果key相等,定位value的位置,在map中找到了key-value,然后返回
  76.          if t.key.equal(key, k) {
  77.             e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
  78.             if t.indirectelem() { // 如果value是指针,那么解引用
  79.                e = *((*unsafe.Pointer)(e))
  80.             }
  81.             return e, true
  82.          }
  83.       }
  84.    }
  85.    // 如果遍历完所有的bucket(包括溢出桶),还没找到,则返回零值
  86.    return unsafe.Pointer(&zeroVal[0]), false
  87. }
复制代码
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 使用率不高,值存储得比较稀疏。在查找时效率会下降。
  1. // 装载因子超过 6.5
  2. func overLoadFactor(count int64, B uint8) bool {
  3.    return count >= bucketCnt && float32(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
  4. }

  5. // overflow buckets 太多
  6. func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
  7.    if B > 15 {
  8.       B = 15
  9.    }
  10.    // The compiler doesn't see here that B < 16; mask B to generate shorter shift code.
  11.    return noverflow >= uint16(1)<<(B&15)
  12. }
复制代码
针对上述两种情况,采用了不同的解决方法:

  • 针对 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也完成迁移。
  1. func hashGrow(t *maptype, h *hmap) {
  2.    // If we've hit the load factor, get bigger.
  3.    // Otherwise, there are too many overflow buckets, so keep the same number of buckets and "grow" laterally.
  4.    // 如果 load factor 超过了阈值,那么需要对 map 进行扩容,即 B = B + 1,bucket 总数会变为原来的二倍
  5.    // 如果还没到阈值,那么只需要保持相同数量的 bucket,横向拍平就行了(等量扩容)
  6.    bigger := uint8(1)
  7.    if !overLoadFactor(h.count+1, h.B) {
  8.       bigger = 0
  9.       h.flags |= sameSizeGrow // 如果flags原先的值小于sameSizeGrow,h.flags += sameSizeGrow
  10.    }

  11.    // 将原先的bucket分给oldbuckets,然后申请新的bucket内存
  12.    oldbuckets := h.buckets
  13.    newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

  14.    // 先把 h.flags 中 iterator 和 oldIterator 对应位清 0,然后如果发现 iterator 位为 1,那就把它转接到 oldIterator 位,使得 oldIterator 标志位变成 1。
  15.    // 潜台词就是:buckets 现在挂到了 oldBuckets 名下了,对应的标志位也转接过去吧。
  16.    flags := h.flags &^ (iterator | oldIterator)
  17.    if h.flags&iterator != 0 {
  18.       flags |= oldIterator
  19.    }

  20.    // 更新map的信息
  21.    h.B += bigger
  22.    h.flags = flags
  23.    h.oldbuckets = oldbuckets
  24.    h.buckets = newbuckets
  25.    h.nevacuate = 0
  26.    h.noverflow = 0

  27.    // 将原先的overflow搬到oldoverflow
  28.    if h.extra != nil && h.extra.overflow != nil {
  29.       // Promote current overflow buckets to the old generation.
  30.       if h.extra.oldoverflow != nil {
  31.          throw("oldoverflow is not nil")
  32.       }
  33.       h.extra.oldoverflow = h.extra.overflow
  34.       h.extra.overflow = nil
  35.    }
  36.    // 如果新的bucket有overflow bucket,赋值给nextOverflow
  37.    if nextOverflow != nil {
  38.       if h.extra == nil {
  39.          h.extra = new(mapextra)
  40.       }
  41.       h.extra.nextOverflow = nextOverflow
  42.    }

  43.    // the actual copying of the hash table data is done incrementally by growWork() and evacuate().
  44.    // 实际的哈希表元素的拷贝工作是在 growWork 和 evacuate 中增量慢慢地进行的
  45. }
复制代码
3.4.3 evacuate函数

evacuate函数实现迁移的核心点在于:

    1. evacDst
    复制代码
    结构:跟踪迁移目标位置
等量扩容:只使用
  1. x
复制代码
(相同索引位置,平行迁移)
翻倍扩容:使用
  1. x
复制代码
  1. y
复制代码
两个目标,将原来一个索引下的bucket内容分配到新bmap数组的两个相关索引下:

    1. x
    复制代码
    :新桶数组低位(索引 =
    1. oldbucket
    复制代码

    1. y
    复制代码
    :新桶数组高位(索引 =
    1. oldbucket + newbit
    复制代码

在翻倍扩容的时候,虽然键的哈希值没有变化,但通过精妙的迁移策略和新索引计算机制,仍然能确保键的正确查找!翻倍扩容时,新掩码(newbit) = 1 << B(B 是旧桶数量的指数),迁移到新桶数组到高位或地位,决定于hash & newbit。然后在查找时,使用新掩码计算索引,在不改变hash函数的情况完美解决查找问题。(新旧mask的区别就是:新mask比旧值多了 B+1 bit位的判断,后 B bit位还是保持一致,所以可以利用hash的第 B+1位值来确定key迁移到低位还是高位)
  1. 新索引 = {
  2. 旧索引 (当 hash & newbit == 0 时)
  3. 旧索引 + newbit (当 hash & newbit != 0 时)
  4. } ======> 新索引 = hash & (newMask),新mask的第 B+1 bit位的值 == 2^B
复制代码
  1. newMask = (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 & 0b00111 (0b01)迁移决策h & newbit = 0b1101 & 0b01004 (非0) → 迁移到高位新索引h & 0b111 = 0b1101 & 0b01115 (0b101)验证旧索引(1) + newbit(4)5
6.jpeg
  1. // evacDst is an evacuation destination.
  2. type evacDst struct {
  3.    b *bmap          // current destination bucket
  4.    i int            // key/elem index into b
  5.    k unsafe.Pointer // pointer to current key storage
  6.    e unsafe.Pointer // pointer to current elem storage
  7. }

  8. // 该函数用于实现hmap扩容时,数据的搬迁工作
  9. // 如果是等量扩容,那么就初始化一个evacDst
  10. // 如果是翻倍扩容,那么就初始化两个evacDst
  11. func evacuate(t *maptype, h *hmap, oldbucket uintptr) {
  12.    // 定位旧bucket的地址以及扩容前map的容量
  13.    b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
  14.    newbit := h.noldbuckets()
  15.    // 如果 b 没有被搬迁过
  16.    if !evacuated(b) {
  17.       // xy contains the x and y (low and high) evacuation destinations.
  18.       // xy 包含的是移动的目标
  19.       // x 表示新 bucket 数组的前(low)半部分,y 表示新 bucket 数组的后(high)半部分
  20.       var xy [2]evacDst
  21.       x := &xy[0]
  22.       x.b = (*bmap)(add(h.buckets, oldbucket*uintptr(t.bucketsize)))
  23.       x.k = add(unsafe.Pointer(x.b), dataOffset)
  24.       x.e = add(x.k, bucketCnt*uintptr(t.keysize))

  25.       if !h.sameSizeGrow() {
  26.          // Only calculate y pointers if we're growing bigger.
  27.          // Otherwise GC can see bad pointers.
  28.          // 如果不是等 size 扩容,前后 bucket 序号有变,使用 y 来进行搬迁
  29.          // 扩容后的map bucket数量是原先的两倍,x,y分别各占一半,数量与扩容前一样
  30.          // y部分桶的编号: oldbucket+newbit
  31.          y := &xy[1]
  32.          y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
  33.          y.k = add(unsafe.Pointer(y.b), dataOffset)
  34.          y.e = add(y.k, bucketCnt*uintptr(t.keysize))
  35.       }

  36.       // 遍历所有的 bucket,包括 overflow buckets,b 是老的 bucket 地址
  37.       for ; b != nil; b = b.overflow(t) {
  38.          // 从oldbucket的第一个cell开始
  39.          k := add(unsafe.Pointer(b), dataOffset)
  40.          e := add(k, bucketCnt*uintptr(t.keysize))
  41.          // 遍历 bucket 中的所有 cell
  42.          for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
  43.             // 当前 cell 的 top hash 值
  44.             top := b.tophash[i]
  45.             // 如果 cell 为空,即没有 key
  46.             if isEmpty(top) {
  47.                // 那就标志它被"搬迁"过,继续下一个cell
  48.                b.tophash[i] = evacuatedEmpty
  49.                continue
  50.             }

  51.             // 正常不会出现这种情况
  52.             // 未被搬迁的 cell 只可能是 empty 或是正常的 top hash(大于 minTopHash)
  53.             if top < minTopHash {
  54.                throw("bad map state")
  55.             }
  56.             k2 := k
  57.             if t.indirectkey() {
  58.                k2 = *((*unsafe.Pointer)(k2))
  59.             }

  60.             var useY uint8
  61.             if !h.sameSizeGrow() {
  62.                // Compute hash to make our evacuation decision (whether we need
  63.                // to send this key/elem to bucket x or bucket y).
  64.                // 计算哈希,以判断我们的数据要转移到哪一部分的 bucket,
  65.                // 可能是 x 部分,也可能是 y 部分
  66.                hash := t.hasher(k2, uintptr(h.hash0))
  67.                if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
  68.                   // If key != key (NaNs), then the hash could be (and probably
  69.                   // will be) entirely different from the old hash. Moreover,
  70.                   // it isn't reproducible. Reproducibility is required in the
  71.                   // presence of iterators, as our evacuation decision must
  72.                   // match whatever decision the iterator made.
  73.                   // Fortunately, we have the freedom to send these keys either
  74.                   // way. Also, tophash is meaningless for these kinds of keys.
  75.                   // We let the low bit of tophash drive the evacuation decision.
  76.                   // We recompute a new random tophash for the next level so
  77.                   // these keys will get evenly distributed across all buckets
  78.                   // after multiple grows.
  79.                   useY = top & 1
  80.                   top = tophash(hash)
  81.                } else {
  82.                   // 根据hash & newbit 来确定新bucket的索引,确保迁移完成之后,
  83.                   // 使用原先的hash值 & 新的mask 还能找到正确的索引
  84.                   if hash&newbit != 0 {
  85.                      useY = 1
  86.                   }
  87.                }
  88.             }

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

  92.             // 在oldbucket对应的cell中标记top的搬迁状态
  93.             b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY
  94.             dst := &xy[useY]                 // evacuation destination

  95.             // 当前bucket中已经放满了元素,需要使用overflow bucket
  96.             if dst.i == bucketCnt {
  97.                dst.b = h.newoverflow(t, dst.b)
  98.                dst.i = 0
  99.                dst.k = add(unsafe.Pointer(dst.b), dataOffset)
  100.                dst.e = add(dst.k, bucketCnt*uintptr(t.keysize))
  101.             }
  102.             dst.b.tophash[dst.i&(bucketCnt-1)] = top // mask dst.i as an optimization, to avoid a bounds check
  103.             if t.indirectkey() {
  104.                *(*unsafe.Pointer)(dst.k) = k2 // copy pointer
  105.             } else {
  106.                typedmemmove(t.key, dst.k, k) // copy elem
  107.             }
  108.             if t.indirectelem() {
  109.                *(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
  110.             } else {
  111.                typedmemmove(t.elem, dst.e, e)
  112.             }
  113.             dst.i++
  114.             // These updates might push these pointers past the end of the
  115.             // key or elem arrays.  That's ok, as we have the overflow pointer
  116.             // at the end of the bucket to protect against pointing past the
  117.             // end of the bucket.
  118.             dst.k = add(dst.k, uintptr(t.keysize))
  119.             dst.e = add(dst.e, uintptr(t.elemsize))
  120.          }
  121.       }
  122.       // Unlink the overflow buckets & clear key/elem to help GC.
  123.       // 如果没有协程在使用老的 buckets,就把老 buckets 清除掉,帮助gc
  124.       if h.flags&oldIterator == 0 && t.bucket.ptrdata != 0 {
  125.          b := add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))
  126.          // Preserve b.tophash because the evacuation state is maintained there.
  127.          // 只清除bucket 的 key,value 部分,保留 top hash 部分,指示搬迁状态
  128.          ptr := add(b, dataOffset)
  129.          n := uintptr(t.bucketsize) - dataOffset
  130.          memclrHasPointers(ptr, n)
  131.       }
  132.    }

  133.    // 更新搬迁进度,如果此次搬迁的 bucket 等于当前进度
  134.    if oldbucket == h.nevacuate {
  135.       advanceEvacuationMark(h, t, newbit)
  136.    }
  137. }
复制代码
3.5 map的删除

map的删除主要流程就是需要在key所在的索引bmap链表中查找到对应的值,进行内容的删除释放。这里需要特别注意的是,为了提升查找效率,有一个emptyRest前向传播机制:如果被删除的key后续位置都是emptyRest,则需要将其前面标记为emptyOne的cell升级为emptyRest,表示从这个cell往后不再有数据。
7.jpeg
  1. func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {

  2.    if raceenabled && h != nil  {
  3.       callerpc := getcallerpc()
  4.       pc := funcPC(mapdelete)
  5.       racewritepc(unsafe.Pointer(h), callerpc, pc)
  6.       raceReadObjectPC(t.key, key, callerpc, pc)
  7.    }
  8.    if msanenabled && h != nil{
  9.       msanread(key, t.key.size)
  10.    }

  11.    // 如果map为空,或者元素个数为零,直接返回
  12.    if h == nil || h.count == 0 {
  13.       if t.hashMightPanic() {
  14.          t.hasher(key, 0)
  15.       }
  16.       return
  17.    }

  18.    // 有goroutine在写map时,无法完成删除操作
  19.    if h.flags&hashWriting != 0 {
  20.       throw("concurrent map writes")
  21.    }
  22.    // 计算需要写入的key的hash值
  23.    hash := t.hasher(key, uintptr(h.hash0))

  24.    // 调用hasher函数时,可能发生paninc,因此没法完成一次写操作
  25.    // 如果hasher没有发生panic,那么将flags设置成flags += 4
  26.    h.flags ^= hashWriting

  27.    //  计算低 8 位 hash,根据计算出的 bucketMask 选择对应的 bucket 编号
  28.    bucket := hash & bucketMask(h.B)
  29.    //  如果map正在扩容,则完成搬迁工作
  30.    if h.growing() {
  31.       growWork(t, h, bucket)
  32.    }
  33.    // 计算key对应桶编号的地址,以及hash值的高8位
  34.    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
  35.    bOrig := b
  36.    top := tophash(hash)
  37. search:
  38.    // 两层循环,第一层遍历bucket后面所有的溢出桶
  39.    //         第二层遍历每个桶内部的8个key位置               
  40.    for ; b != nil; b = b.overflow(t) {
  41.       for i := uintptr(0); i < bucketCnt; i++ {
  42.          // 如果top不匹配
  43.          if b.tophash[i] != top {
  44.             // emptyRest标记此cell后面所有的key(包括overflow桶)都为空,此时直接跳出循环
  45.             if b.tophash[i] == emptyRest {
  46.                break search
  47.             }
  48.             continue
  49.          }

  50.          // 如果b.tophash[i] == top,计算key对应的地址
  51.          // k是指针对象,解引用
  52.          k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
  53.          // 为了保存key的原始类型,便于后续的清理
  54.          k2 := k
  55.          if t.indirectkey() {
  56.             k2 = *((*unsafe.Pointer)(k2))
  57.          }

  58.          // 如果相同的 hash 位置的 key 和要插入的 key 字面上不相等
  59.          // 如果两个 key 的首八位后最后八位哈希值一样,就会进行其值比较,算是一种哈希碰撞
  60.          if !t.key.equal(key, k2) {
  61.             continue
  62.          }

  63.          // Only clear key if there are pointers in it.
  64.          if t.indirectkey() {
  65.             *(*unsafe.Pointer)(k) = nil
  66.          } else if t.key.ptrdata != 0 {
  67.             memclrHasPointers(k, t.key.size)
  68.          }
  69.          e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
  70.          if t.indirectelem() {
  71.             *(*unsafe.Pointer)(e) = nil
  72.          } else if t.elem.ptrdata  != 0 {
  73.             memclrHasPointers(e, t.elem.size)
  74.          } else {
  75.             memclrNoHeapPointers(e, t.elem.size)
  76.          }

  77.          // 标记tophash的状态
  78.          b.tophash[i] = emptyOne
  79.          // If the bucket now ends in a bunch of emptyOne states, change those to emptyRest states.
  80.          // It would be nice to make this a separate function, but for loops are not currently inlineable.
  81.          if i == bucketCnt-1 { // 删除的key位于bucket的尾巴
  82.             // 如果后续还有overflow桶,且桶内部还有元素,那么直接跳到notLast
  83.             // 将此tophash标记为emptyOne即可
  84.             if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
  85.                goto notLast
  86.             }
  87.          } else {
  88.             // 如果key不是最后一个,且后续cell还有元素,也直接跳到notLast
  89.             if b.tophash[i+1] != emptyRest {
  90.                goto notLast
  91.             }
  92.          }
  93.          for {
  94.             // 删除的key后面不再有元素,需要将tophash标记为emptyRest
  95.             // 同时往前寻找,并将前面tophash为emptyOne的位置标记为emptyRest
  96.             b.tophash[i] = emptyRest
  97.             if i == 0 {
  98.                if b == bOrig {
  99.                   break // beginning of initial bucket, we're done.
  100.                }
  101.                // Find previous bucket, continue at its last entry.
  102.                c := b
  103.                // 从key的bucket开始,查找当前bucket的前一个桶地址
  104.                for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
  105.                }
  106.                // bucket内部从最后一个cell开始查找tophash为emptyOne的cell
  107.                i = bucketCnt - 1
  108.             } else {
  109.                i--
  110.             }
  111.             // 查找到一个有内容的tophash,结束循环
  112.             if b.tophash[i] != emptyOne {
  113.                break
  114.             }
  115.          }
  116.       notLast:
  117.          h.count--
  118.          // Reset the hash seed to make it more difficult for attackers to
  119.          // repeatedly trigger hash collisions. See issue 25237.
  120.          if h.count == 0 {
  121.             h.hash0 = fastrand()
  122.          }
  123.          break search
  124.       }
  125.    }

  126.    // 禁止并发写
  127.    if h.flags&hashWriting == 0 {
  128.       throw("concurrent map writes")
  129.    }
  130.    // flags对hashWriting按位置0,
  131.    // '^='表示按右边hashWriting二进制为1的位置,置0,其他位置与flags保持一致
  132.    // 还原写操作之前的flags
  133.    h.flags &^= hashWriting
  134. }
复制代码
3.6 map的遍历


3.6.1 hiter结构体

在对map进行遍历时,会借助迭代器hiter辅助完成整个map的循环遍历,其中startBucket标记此次遍历的起始位置,wrapped标记遍历是否从头开始,checkBucket则是用于扩容中进行遍历时,坚持oldBucket中的数据。
  1. type hiter struct {
  2.         // key 指针,保存此次遍历得到的key
  3.         key         unsafe.Pointer
  4.         // value 指针,保存此次遍历得到的value
  5.         value       unsafe.Pointer
  6.         // map 类型,包含如 key size 大小等
  7.         t           *maptype
  8.         // map header
  9.         h           *hmap
  10.         // 初始化时指向的 bucket
  11.         buckets     unsafe.Pointer
  12.         // 当前遍历到的 bmap
  13.         bptr        *bmap
  14.         overflow    [2]*[]*bmap
  15.         // 起始遍历的 bucet 编号
  16.         startBucket uintptr
  17.         // 遍历开始时 cell 的编号(每个 bucket 中有 8 个 cell)
  18.         offset      uint8
  19.         // 是否从头遍历了
  20.         wrapped     bool
  21.         // B 的大小
  22.         B           uint8
  23.         // 指示当前 cell 序号
  24.         i           uint8
  25.         // 指向当前的 bucket
  26.         bucket      uintptr
  27.         // 因为扩容,需要检查的 bucket
  28.         checkBucket uintptr
  29. }
复制代码
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标签继续处理。
  1. func mapiternext(it *hiter) {
  2.    h := it.h
  3.    if raceenabled {
  4.       callerpc := getcallerpc()
  5.       racewritepc(unsafe.Pointer(h), callerpc, funcPC(mapiternext))
  6.    }

  7.    // 如果有goroutine正在写,抛出异常
  8.    if h.flags&hashWriting != 0 {
  9.       throw("concurrent map iteration and writes")
  10.    }

  11.    t := it.t
  12.    bucket := it.bucket
  13.    b := it.bptr
  14.    i := it.i
  15.    checkBucket := it.checkBucket

  16. next:
  17.    // 后续没有overflow bucket需要遍历
  18.    if b == nil {
  19.       // 当前遍历的bucket=startBucket,即完成循环回到最初的位置
  20.       // 且该bucket已经从头到尾遍历完了,map的遍历结束
  21.       if bucket == it.startBucket && it.wrapped {
  22.          // end of iteration
  23.          it.key = nil
  24.          it.elem = nil
  25.          return
  26.       }

  27.       // 如果map正处于扩容状态,还需要遍历oldbucket
  28.       if h.growing() && it.B == h.B {
  29.          // Iterator was started in the middle of a grow, and the grow isn't done yet.
  30.          // If the bucket we're looking at hasn't been filled in yet (i.e. the old
  31.          // bucket hasn't been evacuated) then we need to iterate through the old
  32.          // bucket and only return the ones that will be migrated to this bucket.
  33.          oldbucket := bucket & it.h.oldbucketmask()
  34.          // 根据bucket num在oldbucket的编号位置,计算bucket的地址
  35.          b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
  36.          if !evacuated(b) {
  37.             // 如果oldbucket中此编号的bucket没有完成搬迁,那么标记check
  38.             checkBucket = bucket
  39.          } else {
  40.             // 如果oldbucket中此编号的bucket完成了搬迁,直接遍历newbucket中对应位置的bucket
  41.             b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
  42.             checkBucket = noCheck
  43.          }
  44.       } else {
  45.          b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
  46.          checkBucket = noCheck
  47.       }
  48.       // 指向下一个bucket
  49.       bucket++
  50.       // 判断是否已经遍历到了末尾,如果是,则需要从头重新开始遍历,
  51.       // 直至bucket == startbucket,标记一次完整的遍历
  52.       if bucket == bucketShift(it.B) {
  53.          bucket = 0
  54.          it.wrapped = true
  55.       }
  56.       i = 0
  57.    }

  58.    // 遍历bucket的8个cell,但不是从头开始遍历的
  59.    for ; i < bucketCnt; i++ {
  60.       offi := (i + it.offset) & (bucketCnt - 1)
  61.       // 如果tophash为空,即没有数据,直接检查下一个
  62.       if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {
  63.          // TODO: emptyRest is hard to use here, as we start iterating
  64.          // in the middle of a bucket. It's feasible, just tricky.
  65.          continue
  66.       }
  67.       k := add(unsafe.Pointer(b), dataOffset+uintptr(offi)*uintptr(t.keysize))
  68.       if t.indirectkey() {
  69.          k = *((*unsafe.Pointer)(k))
  70.       }

  71.       e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+uintptr(offi)*uintptr(t.elemsize))
  72.       if checkBucket != noCheck && !h.sameSizeGrow() {
  73.          // Special case: iterator was started during a grow to a larger size
  74.          // and the grow is not done yet. We're working on a bucket whose
  75.          // oldbucket has not been evacuated yet. Or at least, it wasn't
  76.          // evacuated when we started the bucket. So we're iterating
  77.          // through the oldbucket, skipping any keys that will go
  78.          // to the other new bucket (each oldbucket expands to two buckets during a grow).
  79.          if t.reflexivekey() || t.key.equal(k, k) {
  80.             // If the item in the oldbucket is not destined for the current new bucket in the iteration, skip it.
  81.             hash := t.hasher(k, uintptr(h.hash0))
  82.             if hash&bucketMask(it.B) != checkBucket {
  83.                continue
  84.             }
  85.          } else {
  86.             // Hash isn't repeatable if k != k (NaNs).  We need a
  87.             // repeatable and randomish choice of which direction
  88.             // to send NaNs during evacuation. We'll use the low
  89.             // bit of tophash to decide which way NaNs go.
  90.             // NOTE: this case is why we need two evacuate tophash
  91.             // values, evacuatedX and evacuatedY, that differ in their low bit.
  92.             if checkBucket>>(it.B-1) != uintptr(b.tophash[offi]&1) {
  93.                continue
  94.             }
  95.          }
  96.       }
  97.       if (b.tophash[offi] != evacuatedX && b.tophash[offi] != evacuatedY) ||
  98.          !(t.reflexivekey() || t.key.equal(k, k)) {
  99.          // This is the golden data, we can return it.
  100.          // OR
  101.          // key!=key, so the entry can't be deleted or updated, so we can just return it.
  102.          // That's lucky for us because when key!=key we can't look it up successfully.
  103.          it.key = k
  104.          if t.indirectelem() {
  105.             e = *((*unsafe.Pointer)(e))
  106.          }
  107.          it.elem = e
  108.       } else {
  109.          // The hash table has grown since the iterator was started.
  110.          // The golden data for this key is now somewhere else.
  111.          // Check the current hash table for the data.
  112.          // This code handles the case where the key has been deleted, updated, or deleted and reinserted.
  113.          // NOTE: we need to regrab the key as it has potentially been
  114.          // updated to an equal() but not identical key (e.g. +0.0 vs -0.0).
  115.          rk, re := mapaccessK(t, h, k)
  116.          if rk == nil {
  117.             continue // key has been deleted
  118.          }
  119.          it.key = rk
  120.          it.elem = re
  121.       }
  122.       it.bucket = bucket
  123.       if it.bptr != b { // avoid unnecessary write barrier; see issue 14921
  124.          it.bptr = b
  125.       }
  126.       it.i = i + 1
  127.       it.checkBucket = checkBucket
  128.       return
  129.    }

  130.    // 通过it.i走到此处,遍历overflow bucket
  131.    b = b.overflow(t)
  132.    i = 0
  133.    goto next
  134. }
复制代码
总结

以上为个人经验,希望能给大家一个参考,也希望大家多多支持晓枫资讯。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
晓枫资讯-科技资讯社区-免责声明
免责声明:以上内容为本网站转自其它媒体,相关信息仅为传递更多信息之目的,不代表本网观点,亦不代表本网站赞同其观点或证实其内容的真实性。
      1、注册用户在本社区发表、转载的任何作品仅代表其个人观点,不代表本社区认同其观点。
      2、管理员及版主有权在不事先通知或不经作者准许的情况下删除其在本社区所发表的文章。
      3、本社区的文章部分内容可能来源于网络,仅供大家学习与参考,如有侵权,举报反馈:点击这里给我发消息进行删除处理。
      4、本社区一切资源不代表本站立场,并不代表本站赞同其观点和对其真实性负责。
      5、以上声明内容的最终解释权归《晓枫资讯-科技资讯社区》所有。
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~

  离线 

TA的专栏

等级头衔

等級:晓枫资讯-列兵

在线时间
0 小时

积分成就
威望
0
贡献
0
主题
0
精华
0
金钱
18
积分
16
注册时间
2022-12-27
最后登录
2022-12-27

发表于 2025-10-24 06:33:56 | 显示全部楼层
路过,支持一下
http://bbs.yzwlo.com 晓枫资讯--游戏IT新闻资讯~~~
严禁发布广告,淫秽、色情、赌博、暴力、凶杀、恐怖、间谍及其他违反国家法律法规的内容。!晓枫资讯-社区
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

1楼
2楼

手机版|晓枫资讯--科技资讯社区 本站已运行

CopyRight © 2022-2025 晓枫资讯--科技资讯社区 ( BBS.yzwlo.com ) . All Rights Reserved .

晓枫资讯--科技资讯社区

本站内容由用户自主分享和转载自互联网,转载目的在于传递更多信息,并不代表本网赞同其观点和对其真实性负责。

如有侵权、违反国家法律政策行为,请联系我们,我们会第一时间及时清除和处理! 举报反馈邮箱:点击这里给我发消息

Powered by Discuz! X3.5

快速回复 返回顶部 返回列表