开发者

Go数据结构之映射map方式

目录
  • 1 map的使用
    • 1.1 map基本使用
    • 1.2 map注意事项
  • 2 map的底层数据结构
    • 2.1 bmap
    • 2.2 hmap
  • 3 map实现源码
    • 3.1 map的初始化
    • 3.2 map的赋值
    • 3.3 map的查找
    • 3.4 map的扩容
      • 3.4.1 hashGrow函数
      • 3.4.3 evacuate函数
    • 3.5 map的删除
      • 3.6 map的遍历
        • 3.6.1 hiter结构体
        • 3.6.2 mapiternext函数
    • 总结

      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
      }

      Go数据结构之映射map方式

      桶和溢出桶

      • 桶(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 的指针
      }

      Go数据结构之映射map方式

      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)个溢出桶,需要注意的是正常桶与溢出桶在底层是一个内存地址连续的数组!

      Go数据结构之映射map方式

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

      Go数据结构之映射map方式

      赋值操作底层mapassign函数的主要流程:

      1. 安全检查和初始化

      • 空 map 检查:对 nil map 赋值会 panic
      • 并发写检查:检测到其他 goroutine 正在写 map 时抛出异常(Go map 非并发安全)
      • 哈希计算:使用类型特定的哈希函数计算键的哈希值
      • 标记写操作:设置 hashWriting 标志位(防止并发写)
      • 延迟初始化:若桶数组未分配,分配一个桶(最小化空 map 开销)

      2. 定位桶和搜索键

      双层循环搜索:外层遍历对应index下所有的bucket,内层循环处理每个桶的 8 个槽。

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

      Go数据结构之映射map方式

      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 www.devze.com位,使得 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")
       python     }
            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(相同索引位置,平行迁移)

      翻倍扩容:使用 xy 两个目标,将原来一个索引下的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^B
      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

      Go数据结构之映射map方式

      // 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 < http://www.devze.comminTopHash {
                     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往后不再有数据。

      Go数据结构之映射map方式

      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 w编程客栈hich 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
      }

      总结

      以上为个人经验,希望能给大家一个参考,也希望大家多多支持编程客栈(www.devze.com)。

      0

      上一篇:

      下一篇:

      精彩评论

      暂无评论...
      验证码 换一张
      取 消

      最新开发

      开发排行榜