var mp map[string]interface{}  // 声明fmt.Println(mp, mp == nil, len(mp))   // 输出:nil true 0   mp := make(map[string]interface{})  // make初始化mp := make(map[string]interface{}, 1)  // make初始化并指定容量fmt.Println(mp, mp == nil, len(mp)) // 输出:[] false 0 
mp = map[string]interface{}{}  // 等同于 make(map[string]interface{})mp = map[string]interface{}{  // 字面量初始化 "aa": 11, "bb": 22,}

map可以使用make和字面量的方式进行初始化,初始化的时候可以指定容量,也可以不指定容量(建议如果知道map的容量要提前指定,map本身也有扩容和缩容机制,频繁扩缩也会对map读写性能造成影响,下文会讲解),如果map不进行初始化则不能直接使用,否则可能会造成panic。

1.2map增删改查

var m1 map[int]intm1Res, ok := m1[1]  // 取值时第一个为key对应的val,第二个是map中是否存在key   fmt.Println(m1Res, ok) // 输出: 0,false 未初始化的map获得类型的零值m1[1] = 1  // 运行时报panic: assignment to entry in nil map
// 增加数据, len()参数是map类型时获取的是map中存储的键值对数量m1 = make(map[int]int)m1[1] = 1fmt.Println(m1, len(m1))  // 输出: [1:1] 1m1[2] = 2fmt.Println(m1, len(m1))  // 输出:[1:1 2:2] 2
// 删除数据delete(m1, 1) fmt.Println(m1, len(m1))  // 输出:[2:2] 1
// 修改数据m1[2] = 3fmt.Println(m1, len(m1)) // 输出:[2:3] 1
// 遍历mapm1[4] = 4m1[5] = 5m1[6] = 6 // Map是无序的,每次遍历的顺序都不一样for key, val := range m1 {   fmt.Printf("%d:%dn", key, val)}

map的增、改、查均没有提供相应的操作函数,使用起来非常方便(在编译时会由编译器进行代码优化,运行时动态调用/map.go对应的方法完成对map内存分配、增、删、改、查、遍历等操作),除此之外,map的key一定要是比较的,不能是slice、map和func等数据类型。

由于map这种的数据类型十分灵活,同时借助于go中特有的空接口类型,所以在生产实践中,在对不同结构体对象操作时,我们经常会将和map进行转换,方便数据传输。

type person struct {    Name string `json:"name"`    Age  int    `json:"age"`  }
p := person{ Name: "zhangsan", Age: 25, }
  // 作为反序列的输出地址,此时不用初始化map  var m2 map[string]interface{}  bytes, _ := json.Marshal(p) json.Unmarshal(bytes, &m2)  fmt.Println(m2, len(m2)) // 输出:[age:25 name:zhangsan] 2

同时go中的map还可以作为set集合使用,实现一个无重复元素的集合,并且增删改查的时间复杂度都是O(1)。

// struct{}不占用空间// 可以作为一个空的占位符,在go中的使用场景非常广泛var set map[string]struct{}
set["aa"] = struct{}set["bb"] = struct{}
if _, ok := set["cc"]; ok{  ...}

2. map源码剖析

2.1 map底层数据结构

map的底层是一个hash桶的结构,用于存储key-val键值对数据(本文中的map源码剖析基于go1.19),map的核心数据和代码位于 /map.go中。

// map的运行时数据结构type hmap struct {    count     int  // 当前map中键值对的数量  flags     uint8  // 标记map的状态,并发状态等  B         uint8  // buckets数量的对数 buckets = 2^B  noverflow uint16 // 溢出桶的数量  hash0     uint32 // hash seed
buckets unsafe.Pointer // 当前map对应桶(bucket)的指针  // map扩容时用于存储旧桶的数据,扩容完成则为nil oldbuckets unsafe.Pointer   // 扩容时使用,用于标识旧桶中小于nevacuate的数据都已经转移到新桶 nevacuate uintptr   // 存储map溢出桶 extra *mapextra // optional fields}
// bucket运行时数据结构,根据tophash定位key-val键值对type bmap struct {  tophash [bucketCnt]uint8}

map底层的核心数据结构是hmap和bmap,hmap中包含键值对数量count、hash桶、hash桶数量B等关键参数,其中的类型是. 底层引用的数据结构是bmap,bmap是用来查找key-val键值对的数据结构,内部只存储了键值对的。

虽然bmap结构体没有直接存储key-val键值对信息,但是我们依然可以通过找到key-val键值对的位置。让我们从头过一下通过key定位key-val的大致过程。首先map运行时会根据用户传入的key根据hash函数计算出hash值,bmap中的数组存储的正是hash值的高8位,在根据hash值对长度取余求出该key所在数组中的的位置,在通过遍历该的可以大致确定是否存在key,不存在直接返回,相等的话map.entry,在判断对应位置的key是否相等,key相等的话再通过内存偏移即可找到对应val(map的key和val数据类型是既定的,内存空间也是已知的,并且key-val键值对存储在中一片连续的内存,因此可以通过内存偏移间接找到key-val键值对的存储位置)。接下来让我们先来看看map内存分配的源码。

2.2 map内存分配

func makemap(t *maptype, hint int, h *hmap) *hmap {  // 内存溢出判断  mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)  if overflow || mem > maxAlloc {    hint = 0  }
// initialize Hmap if h == nil { h = new(hmap) }  // 生成hash因子 h.hash0 = fastrand()
  // 找到满足bucket不过载的最小B B := uint8(0) for overLoadFactor(hint, B) { B++ } h.B = B
   if h.B != 0 { var nextOverflow *bmap    // 根据B创建buckets和溢出buckets h.buckets, nextOverflow = makeBucketArray(t, h.B, nil) if nextOverflow != nil { h.extra = new(mapextra) h.extra.nextOverflow = nextOverflow } }
return h}
// 判断bucket是否过载 count > 6.5 * 2^Bfunc overLoadFactor(count int, B uint8) bool { return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)}
// 创建hash桶和溢出桶func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) { base := bucketShift(b) nbuckets := base   // 如果make(map[int]int, hint)时指定的容量 hint > 52 则会创建溢出桶 if b >= 4 { // Add on the estimated number of overflow buckets // required to insert the median number of elements // used with this value of b. 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) } }
if base != nbuckets {    // 通过内存偏移找到溢出桶的位置,并把最后一个溢出桶指向buckets开头的位置 nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize))) last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize))) last.setoverflow(t, (*bmap)(buckets)) } return buckets, nextOverflow}

map初始化的对应源码是/map.go:()函数,核心流程就是:根据用户初始化map时指定的容量hint确定保证map不过载的最小B(即hint > 6.5 * 2^B),然后计算出需要创建的和溢出的数量,其中是一片连续的内存(桶数组),溢出桶放在数组最后面的位置。

2.3map数据读取

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {    ...  // map未初始化或者map中没有键值对,直接返回val类型零值  if h == nil || h.count == 0 {    if t.hashMightPanic() {      t.hasher(key, 0) // see issue 23734    }    return unsafe.Pointer(&zeroVal[0])  }  // 校验flag是否是4,是的话说明该map正在被写入  if h.flags&hashWriting != 0 {    fatal("concurrent map read and map write")  }  // 根据hash因子,计算key的哈希值  hash := t.hasher(key, uintptr(h.hash0))  // m = 2^B-1  m := bucketMask(h.B)  // 通过内存偏移找到key对应的bucket的位置  // hash & m 就是key落在的hash桶的位置  b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))  if c := h.oldbuckets; c != nil {  // 说明map正在迁移中    // h.flag == 8说明map在等量扩容,否则是增量扩容    if !h.sameSizeGrow() {      // 增量扩容下m = m / 2      m >>= 1    }    // 获得该key对应的旧桶的位置    oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))    // 该旧桶为迁移完成,就从旧桶中遍历数据    if !evacuated(oldb) {      b = oldb    }  }  // 获得hash的top8位  top := tophash(hash)bucketloop:  // 大循环遍历桶和溢出桶链表,如果有溢出桶的话,没有就只遍历一个桶  for ; b != nil; b = b.overflow(t) {    // 小循环遍历每个桶内的8个数据,根据tophash定位    for i := uintptr(0); i < bucketCnt; i++ {      if b.tophash[i] != top {        // 当前位置的tophash是emptyReset代表后续槽内都没有值了        // 直接退出遍历        if b.tophash[i] == emptyRest {          break bucketloop        }        // 否则遍历下一个tophash        continue      }      // 找到key的内存地址      k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))      if t.indirectkey() {        k = *((*unsafe.Pointer)(k))      }      if t.key.equal(key, k) {        // 找到val的内存地址        e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))        if t.indirectelem() {          e = *((*unsafe.Pointer)(e))        }        return e      }    }  }  // 桶和溢出桶都遍历完,没找到数据,返回val类型零值  return unsafe.Pointer(&zeroVal[0])}
// 根据桶的第一个hash的值判断是否迁移完成  1<h<5代表已迁移完成func evacuated(b *bmap) bool { h := b.tophash[0] return h > emptyOne && h < minTopHash}

map读数据的核心源码是/map.go:()函数,总体流程就是:首先判断是否有map写冲突,然后根据key计算hash值,对桶数组长度取余找到key所存储的hash桶的对应位置(取余通过hash & m实现),然后根据遍历桶上的,通过内存偏移找到对应的key-val键值对数据,没找到返回val类型零值。需要注意的是,如果读map时,map正在迁移或者重建,则会判断是否需要去旧桶上读数据。

2.4 map数据写流程

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {  // 空map执行写操作直接panic  if h == nil {    panic(plainError("assignment to entry in nil map"))  }  ...  // 如果有同时对map的写操作,直接fatal error  if h.flags&hashWriting != 0 {    fatal("concurrent map writes")  }  // 根据key获得hash因子  hash := t.hasher(key, uintptr(h.hash0))
  // map写操作需要设置flag状态位 h.flags ^= hashWriting
  // 如果初始化map时容量<=6,则会lazy initialize if h.buckets == nil { h.buckets = newobject(t.bucket) // newarray(t.bucket, 1) }
again: bucket := hash & bucketMask(h.B)  // 如果map正在扩容,会先执行一部分扩容工作 if h.growing() { growWork(t, h, bucket) }  // 获得需要分配的hash桶, 并计算tophash b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) top := tophash(hash)
var inserti *uint8 var insertk unsafe.Pointer var elem unsafe.Pointerbucketloop:  // 大循环遍历该位置的hash桶链表 for {    // 小循环遍历每个桶的tophash for i := uintptr(0); i < bucketCnt; i++ { if b.tophash[i] != top { if isEmpty(b.tophash[i]) && inserti == nil {          // 找到一个空的槽,记录一下,当inserti为空时 inserti = &b.tophash[i] insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize)) }        // 如果后续所有槽都为空,包含溢出桶的槽,则直接退出遍历 if b.tophash[i] == emptyRest { break bucketloop }        // 遍历当前桶的下一个槽 continue } k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) if t.indirectkey() { k = *((*unsafe.Pointer)(k)) }      // key不相等,则遍历下一个槽 if !t.key.equal(key, k) { continue } // already have a mapping for key. Update it. if t.needkeyupdate() { typedmemmove(t.key, k, key) }      // key相等,找到对应位置的val elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize)) goto done } ovf := b.overflow(t) if ovf == nil { break } b = ovf }
  // 没找当相同的key,会在插入前,判断是否需要map扩容或者重建 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 }
  // 没有找到插入位置,则创建溢出桶,接收新的key和val if inserti == nil { // 获得一个溢出桶 newb := h.newoverflow(t, b) inserti = &newb.tophash[0] insertk = add(unsafe.Pointer(newb), dataOffset) elem = add(insertk, bucketCnt*uintptr(t.keysize)) }
...   // 将key放到指定的位置 typedmemmove(t.key, insertk, key)  // 将tophash放到指定的位置上 *inserti = top  // map数量加一 h.count++
// 判断并发标记、设置写状态位、返回key位置的valdone: if h.flags&hashWriting == 0 { fatal("concurrent map writes") }  // flags恢复初始状态 h.flags &^= hashWriting if t.indirectelem() { elem = *((*unsafe.Pointer)(elem)) } return elem}

map写流程的核心源码位于/map.go:()中,总体流程是:nil map直接返回,求出key对应的hash值,设置写map状态位flag,如果map处于扩容中会完成一部分扩容任务,然后遍历hash桶链表中的每一个,存在key则会返回key对应val所在的位置,后续更新,不存在则会记录下,key和val要插入的位置,判断是否需要对map进行扩容和重建,以及是否需要创建溢出桶去承载新的key-val键值对,后续对和key执行插入动作,val对应的位置进行返回。

func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {  var ovf *bmap  if h.extra != nil && h.extra.nextOverflow != nil {    // 已存在溢出桶,直接获取    ovf = h.extra.nextOverflow    if ovf.overflow(t) == nil {      // 存在后续的溢出桶      // 将当前溢出桶指向下一个溢出桶,因为溢出桶分配的时候是以数组的形式分配,放置在buckets数组的尾部      h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))    } else {      // 不存在后续的溢出桶      ovf.setoverflow(t, nil)      h.extra.nextOverflow = nil    }  } else {    // extra中没有溢出桶,新建    ovf = (*bmap)(newobject(t.bucket))  }  // 溢出桶数量加1  h.incrnoverflow()  if t.bucket.ptrdata == 0 {    h.createOverflow()    *h.extra.overflow = append(*h.extra.overflow, ovf)  }  // 将溢出桶放在当前桶的尾部  b.setoverflow(t, ovf)  return ovf}

获取溢出桶的逻辑是:先从hmap的extra中获取(初始化时会根据容量动态设置),获取到的话,设置extra的指向,获取不到就新建一个溢出桶,最后会将溢出桶数量加1,并将其设置在当前桶的位置,形成桶链表。

2.5 map删除key-val流程

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {  ...  // 空map或者map键值对数量为0, 直接返回  if h == nil || h.count == 0 {    if t.hashMightPanic() {      t.hasher(key, 0) // see issue 23734    }    return  }  // 校验flag状态为是否有写冲突  if h.flags&hashWriting != 0 {    fatal("concurrent map writes")  }
  // 根据hash种子计算哈希函数 hash := t.hasher(key, uintptr(h.hash0))
  // 设置flag状态位为正在写 h.flags ^= hashWriting
  // 求出key所在bucket的桶位置(index) bucket := hash & bucketMask(h.B) if h.growing() {    // 如果正在扩容,会先完成一部分扩容工作,再删除 growWork(t, h, bucket) }  // 得到bucket位置的hash桶 b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize))) bOrig := b  // 得到key的tophash 即hash值的前8位 top := tophash(hash)search:  // 大循环遍历当前bucket和溢出bucket链表 for ; b != nil; b = b.overflow(t) {    // 小循环遍历每个桶中的hash槽 for i := uintptr(0); i < bucketCnt; i++ { if b.tophash[i] != top {        // 一旦发现某个tophash槽中的值是emptyRest,        // 则直接退出循环 if b.tophash[i] == emptyRest { break search } // 找下一个tophash槽 continue }      // 找到key的位置 k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize)) k2 := k if t.indirectkey() { k2 = *((*unsafe.Pointer)(k2)) } if !t.key.equal(key, k2) {        // key不相等,直接判断下一个tophash槽的key continue }      // 删除key和val 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) }      // 删除的key对应位置的tophash会被置为emptyone或emptyReset b.tophash[i] = emptyOne       // 删除时会向前回溯,将需要的状态位置为emptyReset, 加速查找      if i == bucketCnt-1 { if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest { // 不需要向前回溯,直接结束 goto notLast }         } else { if b.tophash[i+1] != emptyRest { goto notLast } }       // 当 i==bucketCnt-1 当前bucket没有溢出桶,或者溢出桶的第一个      // tophash已经是emptyReset时;或者i != bucketCnt-1 当前位置      // 的下一个tophash是emptyReset时 进入这个for循环 for { // 将emptyOne更改为emptyReset b.tophash[i] = emptyRest        // 如果是当前桶链表的第一个桶的第一个tophash槽,直接结束 if i == 0 { if b == bOrig { break // beginning of initial bucket, we're done. } c := b          // 向前回溯,找到桶链表当前位置的上一个桶 for b = bOrig; b.overflow(t) != c; b = b.overflow(t) { }          // 从桶的尾部开始向前遍历 i = bucketCnt - 1 } else { i-- }        // 只有tophash的状态是emptyOne才能转换为emptyReset,否则直接结束 if b.tophash[i] != emptyOne { break } }       // 将count-- 重置hash因子, 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 } }
  // 判断是否有并发map写冲突 if h.flags&hashWriting == 0 { fatal("concurrent map writes") }  // 将map flag状态位恢复初始状态 h.flags &^= hashWriting}

map删除key-val键值对的核心源码位于/map.go:()方法中,核心流程是:通过hash函数和位运算找到key对应的hash桶的位置,如果map在扩容中会先完成一部分扩容工作,然后遍历溢出桶链表中的每一个hash槽,一旦发现直接结束遍历(加速遍历),否则的话,如果找到了key,将当前置为,key和val内存或者指针清空,然后回从当前当前桶链表的当前位置开始向前遍历,判断是否设置状态位(还是为了加速遍历,放到中做这个事比较合适) ,最后会将map的count--,判断没有并发写冲突后,并恢复flag状态位。

2.6 map遍历流程

map.entry_map.entry_map.entry

// 核心数据结构type hiter struct {  key         unsafe.Pointer // 遍历到key的指针  elem        unsafe.Pointer // 遍历到val的指针  t           *maptype       //  map类型  h           *hmap          // hmap  buckets     unsafe.Pointer // map桶数组  bptr        *bmap          // 当前遍历到的桶  overflow    *[]*bmap       // 新map的溢出桶  oldoverflow *[]*bmap       // 老map的溢出桶  startBucket uintptr        // 遍历起始位置的桶索引  offset      uint8          // 遍历起始位置的key-val对索引  wrapped     bool           // 遍历是否穿越了桶数组尾部返回到了头部  B           uint8          // 桶数组长度指数   i           uint8          // 当前遍历到key-val对的在桶中的索引  bucket      uintptr        // 当前遍历到的bucket 索引  checkBucket uintptr        //  扩容时需要额外检查的桶}

map遍历时通过设置一个随机数,找到起始遍历的位置和中起始遍历的hash槽的位置,然后按上图规则遍历,由于每次遍历随机因子的不同,也保证了每次遍历顺序的不同。

2.7 map的扩容和重建

map扩容分为增量扩容和等量重建,会在增加key-val键值对时对map判断,满足条件后触发。

增量扩容:map的桶数组的长度会增长到原来的两倍,以保证整体数组的负载不要太高。触发方式:hmap.count > 6.5 * 2 ^B。

func overLoadFactor(count int, B uint8) bool {  return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)}

等量重建:为避免map在多次增删操作后,内部hash槽的空洞太多(会导致内存泄漏),桶链表过长,对map桶数组进行等容量,提升空间利用率。触发方式:hmap. > 2^B。

func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {    if B > 15 {    B = 15  }    return noverflow >= uint16(1)<<(B&15)}

开启扩容的时机是map新增数据时,开始扩容的源代码位于为/map.go:()中。

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  }
func hashGrow(t *maptype, h *hmap) {   // bigger:1 增量扩容 bigger:0 等量重建 bigger := uint8(1) if !overLoadFactor(h.count+1, h.B) { bigger = 0 h.flags |= sameSizeGrow }  // 当前buckets赋值给oldbuckets oldbuckets := h.buckets  // 创建新的buckets和溢出buckets newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
flags := h.flags &^ (iterator | oldIterator) if h.flags&iterator != 0 { flags |= oldIterator }  // 字符重新初始化 h.B += bigger h.flags = flags h.oldbuckets = oldbuckets h.buckets = newbuckets h.nevacuate = 0 h.noverflow = 0
if h.extra != nil && h.extra.overflow != nil { // Promote current overflow buckets to the old generation. if h.extra.oldoverflow != nil { throw("oldoverflow is not nil") } // 存量可用的溢出桶赋值给oldoverflow h.extra.oldoverflow = h.extra.overflow h.extra.overflow = nil } if nextOverflow != nil { if h.extra == nil { h.extra = new(mapextra) }    // 存在下一个可用的溢出桶,则赋值给nextOverflow h.extra.nextOverflow = nextOverflow }
// the actual copying of the hash table data is done incrementally // by growWork() and evacuate().}

可以看到,()函数只是做了新分配内存和一些字段的初始化的工作,也间接说明了map采用渐进式扩容的策略,在插入、更新和删除时执行真正的数据迁移动作。

func growWork(t *maptype, h *hmap, bucket uintptr) {  // 第一次迁移当前bucket对应的oldbucket  evacuate(t, h, bucket&h.oldbucketmask())
// evacuate one more oldbucket to make progress on growing if h.growing() {    // 第二次迁移当前未迁移桶中索引最小的桶 evacuate(t, h, h.nevacuate) }}
func evacuate(t *maptype, h *hmap, oldbucket uintptr) { b := (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize))) newbit := h.noldbuckets() if !evacuated(b) { // TODO: reuse overflow buckets instead of using new ones, if there // is no iterator using the old buckets. (If !oldIterator.)
// xy contains the x and y (low and high) evacuation destinations. 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. y := &xy[1] y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize))) y.k = add(unsafe.Pointer(y.b), dataOffset) y.e = add(y.k, bucketCnt*uintptr(t.keysize)) }
for ; b != nil; b = b.overflow(t) { k := add(unsafe.Pointer(b), dataOffset) e := add(k, bucketCnt*uintptr(t.keysize)) for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) { top := b.tophash[i] if isEmpty(top) { b.tophash[i] = evacuatedEmpty continue } if top < minTopHash { throw("bad map state") } k2 := k if t.indirectkey() { k2 = *((*unsafe.Pointer)(k2)) } var useY uint8 if !h.sameSizeGrow() { // Compute hash to make our evacuation decision (whether we need // to send this key/elem to bucket x or bucket y). hash := t.hasher(k2, uintptr(h.hash0)) if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) { useY = top & 1 top = tophash(hash) } else { if hash&newbit != 0 { useY = 1 } } }
if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY { throw("bad evacuatedN") }
b.tophash[i] = evacuatedX + useY // evacuatedX + 1 == evacuatedY dst := &xy[useY] // evacuation destination
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. 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. ptr := add(b, dataOffset) n := uintptr(t.bucketsize) - dataOffset memclrHasPointers(ptr, n) } }
if oldbucket == h.nevacuate { advanceEvacuationMark(h, t, newbit) }}

map.entry_map.entry_map.entry

map.entry_map.entry_map.entry

3.总结

map是go语言使用十分方便的key-val键值对数据结构,增删改查的时间复杂度可以看作是O(1),但是其内部实现原理确并不简单。日常使用中map.entry,如果提前已知map的存储容量,初始化时指定容量大小,可以减少频繁扩容带来的读写性能抖动影响。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注