Go-Map源码解读

前言

一般的map都是采用数组+链表的数据结构去进行数据存储,在单节点挂载数据过多时,会考虑将链表转换成树结构来提高效率。那么GO语言的map是怎样实现的呢?有用到树结构吗?在动态扩缩容时又是怎样进行数据迁移的呢?咱们带着这几个问题去了解一下下面的内容。

数据结构

// map的类型
type maptype struct {
    typ        _type
    key        *_type // 键的类型
    elem       *_type // 值的类型
    bucket     *_type // hash桶的内部类型
    keysize    uint8  // key的尺寸
    valuesize  uint8  // value的尺寸
    bucketsize uint16 // bucket的尺寸
    flags      uint32
}
// A header for a Go map.
type hmap struct {
    count     int // map中元素的个数
    flags     uint8 // 操作map过程中的标记位
    B         uint8  // 桶的数量的2^B个
    noverflow uint16 // 溢出桶的数量
    hash0     uint32 // hash种子

    buckets    unsafe.Pointer //  2^B 大小的数组,如果 count == 0 的话,可能是 nil
    oldbuckets unsafe.Pointer // 扩容前的bucket数组
    nevacuate  uintptr        // 扩容过程中的一个计数器

    extra *mapextra // optional fields
}
const(
    bucketCntBits = 3  
    bucketCnt     = 1 << bucketCntBits // 一个 bucket 最多能放的元素数

    // load factor = 13/2
    loadFactorNum = 13
    loadFactorDen = 2

    // 超过这两个 size 的对应对象,会被转为指针
    maxKeySize   = 128
    maxValueSize = 128
    
    emptyRest      = 0 // tophash对应的位置为空,并且后续没有非空的值
    emptyOne       = 1 // tophash对应的位置为空,元素被删除后可能的标记值
    evacuatedX     = 2 // key/value是无效的,值被迁移到新table的前半段
    evacuatedY     = 3 // key/value是无效的,值被迁移到新table的后半段
    evacuatedEmpty = 4 // tophash对应的值为空,但是表示正在迁移中
    minTopHash     = 5 // 一个临界值,前面所有的值都代表一个特殊的含义,所以当top<5时,top+=5
)


// bucket 
type bmap struct {
    // tophash 是 hash 值的高 8 位
    tophash [bucketCnt]uint8
    // keys
    // values
    // overflow pointer
}


基本结构如下图所示:

empty
func Test_f(t *testing.T) {
    //m := map[int]string{} // runtime.makemap
    m := make(map[int]string,16)
    m[1] = "test"
    fmt.Println(m)
}

经过简单的编译go tool compile -N -l -S map_test.go后,可以在里面发现这样一段信息:

type.noalg.map.bucket[int]string SRODATA dupok size=176
        rel 24+8 t=1 runtime.algarray+0
        rel 32+8 t=1 runtime.gcbits.00aaaa02+0
        rel 40+4 t=5 type..namedata.*map.bucket[int]string-+0
        rel 44+4 t=6 type.*map.bucket[int]string+0
        rel 48+8 t=1 type..importpath..+0
        rel 56+8 t=1 type.noalg.map.bucket[int]string+80
        rel 80+8 t=1 type..namedata.topbits-+0
        rel 88+8 t=1 type.[8]uint8+0
        rel 104+8 t=1 type..namedata.keys-+0
        rel 112+8 t=1 type.noalg.[8]int+0
        rel 128+8 t=1 type..namedata.values-+0
        rel 136+8 t=1 type.noalg.[8]string+0
        rel 152+8 t=1 type..namedata.overflow-+0
        rel 160+8 t=1 type.*map.bucket[int]string+0

从上面的信息可以看出

  • bmap类型为[8]uint8
  • keys类型为[8]int
  • value类型为[8]string
  • overflow类型为map.bucket[int]string的一个指针

PS:看源码时最好将功能性代码拆开去阅读,比如扩容内容和增、删、查都可以拆开单独去理解,在都能想通的情况下再整合到一起,这样会比较轻松一点。

初始化

make(map[k]v,hint),hint<8时,会调用makemap_small来进行map的初始化;当hint>8时,则会调用makemap进行初始化。

// hint < 8
func makemap_small() *hmap {
    h := new(hmap)
    h.hash0 = fastrand()
    return h
}
// hint > 8
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)
    }
    h.hash0 = fastrand()

    // 找到一个合适的B的值让其能够满足hint对应数量的数据
    // For hint < 0 overLoadFactor returns false since hint < bucketCnt.
    B := uint8(0)
    for overLoadFactor(hint, B) {
        B++
    }
    h.B = B

    // 如果B=0,则在添加数据的时候进行初始化分配内存
    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) {
    base := bucketShift(b)
    nbuckets := base
    // 对于比较小的b而言,不需要分配溢出桶,避免了不必要的计算开销
    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 {
        // 分配一个新的bucket数组
        buckets = newarray(t.bucket, int(nbuckets))
    } else {
        // 这里是在clear方法调用后才会触发,分为清理指针bucket和非指针bucket
        buckets = dirtyalloc
        size := t.bucket.size * nbuckets
        if t.bucket.kind&kindNoPointers == 0 {
            memclrHasPointers(buckets, size)
        } else {
            memclrNoHeapPointers(buckets, size)
        }
    }

    if base != nbuckets {
        // 这里取巧了一点,预先分配的溢出桶的overflow都是nil,我们要想知道哪个溢出桶是
        // 最后一个,必须得给个标记,这里将last.overflow = buckets进行赋值,方便
        // 在分配overflow的时候能够高效的知道已经到了溢出桶的尾部。
        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
}

新增数据

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    if h == nil {
        panic(plainError("assignment to entry in nil map"))
    }
    
    alg := t.key.alg
    hash := alg.hash(key, uintptr(h.hash0))

    // 设置一个标记,防止并发写,后面会检测这个标记
    h.flags ^= hashWriting

    // 懒初始化,在前面初始化map时,B为0,桶数组放到这里来进行初始化
    if h.buckets == nil {
        h.buckets = newobject(t.bucket) // newarray(t.bucket, 1)
    }

again:
    // 获取分配到的桶的位置
    bucket := hash & bucketMask(h.B)
    
    if h.growing() {
        // 进行扩容
        growWork(t, h, bucket)
    }

    // 获取选中桶对应的bmap
    b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
    // 获取该key的高位hash值
    top := tophash(hash)

    var inserti *uint8
    var insertk unsafe.Pointer
    var val unsafe.Pointer
bucketloop:
    for {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                if isEmpty(b.tophash[i]) && inserti == nil {
                    // 记录tophash中最早一次出现empty的情况,这里不一定是emptyRest,有可能是
                    // 删除数据时的emptyOne状态
                    inserti = &b.tophash[i]
                    insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
                    val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
                }
                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))
            }
            // 计算当前k与传进来的key是否相等,有可能只是高位相等,则需要继续循环
            if !alg.equal(key, k) {
                continue
            }
            // key在这个map中,需要更新
            if t.needkeyupdate() {
                typedmemmove(t.key, k, key)
            }
            val = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
            goto done
        }
        // 当前桶已经没有存储数据的位置了,则需要从overflow溢出桶中查询位置
        ovf := b.overflow(t)
        if ovf == nil {
            break
        }
        b = ovf
    }
    // 扩容触发点,下面俩满足一个就行:
    // 1. map中数据量超过了6.5*桶数量
    // 2. 溢出桶数量过多
    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 {
        // 所有的当前桶都是满的,需要构建一个新的溢出桶
        newb := h.newoverflow(t, b)
        inserti = &newb.tophash[0]
        insertk = add(unsafe.Pointer(newb), dataOffset)
        val = add(insertk, bucketCnt*uintptr(t.keysize))
    }

    // store new key/value at insert position
    if t.indirectkey() {
        kmem := newobject(t.key)
        *(*unsafe.Pointer)(insertk) = kmem
        insertk = kmem
    }
    if t.indirectvalue() {
        vmem := newobject(t.elem)
        *(*unsafe.Pointer)(val) = vmem
    }
    typedmemmove(t.key, insertk, key)
    *inserti = top
    // 自增map中元素的个数
    h.count++

done:
    if h.flags&hashWriting == 0 {
        // 并发的写,标记在上面通过h.flags ^= hashWriting设置
        throw("concurrent map writes")
    }
    // 取消并发写的标记
    h.flags &^= hashWriting
    if t.indirectvalue() {
        val = *((*unsafe.Pointer)(val))
    }
    // 返回对应值的地址
    return val
}

empty

结合上面这张图,咱们举个简单的例子,假设hash(key)之后定位到了1号桶,然后高8位的值为top,那么咱们就会遍历1号桶对应的tophash去查询对应的地址。用简单的伪代码表现为:

loop:
    for i:=0;i<8;i++{
        if tophash[i] == top{
           return &bucket+tophash_size+8*key_size+i*value_size  
        }
        if tophash[i] == empty{
            return &bucket+tophash_size+8*key_size+i*value_size  
        }
        //前面都没找到,则要去溢出桶中去找位置,继续循环溢出桶中的tophash
        tophash = overflowBucket.tophash
        goto loop
    }

获取数据

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    // map为空或者元素数量为0时快速返回
    if h == nil || h.count == 0 {
        if t.hashMightPanic() {
            t.key.alg.hash(key, 0) // see issue 23734
        }
        return unsafe.Pointer(&zeroVal[0])
    }
    // 标记并发修改位
    if h.flags&hashWriting != 0 {
        throw("concurrent map read and map write")
    }
    alg := t.key.alg
    hash := alg.hash(key, uintptr(h.hash0))
    m := bucketMask(h.B)
    b := (*bmap)(add(h.buckets, (hash&m)*uintptr(t.bucketsize)))
    if c := h.oldbuckets; c != nil {
        if !h.sameSizeGrow() {
            // 不是等值扩容的情况下,容量扩容为原来的2倍,这里计算时需要将其还原m/2
            m >>= 1
        }
        oldb := (*bmap)(add(c, (hash&m)*uintptr(t.bucketsize)))
        if !evacuated(oldb) {
            // 处于扩容过程中,应该从oldbuckets中去获取数据
            b = oldb
        }
    }
    // 获取hash高8位的值
    top := tophash(hash)
bucketloop:
    for ; b != nil; b = b.overflow(t) {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                // 提前返回,emptyRest表示在它之后不会存在非空值
                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))
            }
            // 防止仅top位相同的hash冲突
            if alg.equal(key, k) {
                v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
                if t.indirectvalue() {
                    v = *((*unsafe.Pointer)(v))
                }
                return v
            }
        }
    }
    return unsafe.Pointer(&zeroVal[0])
}

获取数据代码相对来说比较简单点,首先定位到桶的位置,然后遍历该桶的tophash,查询key对应的位置,当前桶查询不到则继续在其溢出桶中进行递归检索即可。当然这里对扩容那一块做了特殊处理,也就是在定位桶的位置处有点细微差别而已。

删除数据

func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
    if h == nil || h.count == 0 {
        if t.hashMightPanic() {
            t.key.alg.hash(key, 0) // see issue 23734
        }
        return
    }
    // 检测是否有并发写
    if h.flags&hashWriting != 0 {
        throw("concurrent map writes")
    }

    alg := t.key.alg
    hash := alg.hash(key, uintptr(h.hash0))

    // 标记此时正在执行修改操作
    h.flags ^= hashWriting
    // 按低位进行桶的选择
    bucket := hash & bucketMask(h.B)
    
    if h.growing() {
        // 进行扩容
        growWork(t, h, bucket)
    }

    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
    bOrig := b
    top := tophash(hash)
search:
    for ; b != nil; b = b.overflow(t) {
        for i := uintptr(0); i < bucketCnt; i++ {
            if b.tophash[i] != top {
                if b.tophash[i] == emptyRest {
                    break search
                }
                continue
            }
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            k2 := k
            if t.indirectkey() {
                k2 = *((*unsafe.Pointer)(k2))
            }
            if !alg.equal(key, k2) {
                continue
            }
            // key是指针的需要置空,方便GC
            if t.indirectkey() {
                *(*unsafe.Pointer)(k) = nil
            } else if t.key.kind&kindNoPointers == 0 {
                memclrHasPointers(k, t.key.size)
            }
            v := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.valuesize))
            if t.indirectvalue() {
                *(*unsafe.Pointer)(v) = nil
            } else if t.elem.kind&kindNoPointers == 0 {
                memclrHasPointers(v, t.elem.size)
            } else {
                memclrNoHeapPointers(v, t.elem.size)
            }
            // 标记删除位,证明这个位置已被删除
            b.tophash[i] = emptyOne
    
            if i == bucketCnt-1 {
                // 如果是该tophash的最后一个,则需要判断overflow中tophash的第一位是否是emptyRest
                if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
                    goto notLast
                }
            } else {
                // 如果不是当前tophash的最后一个,则直接判断当前tophash的下一个是否是emptyRest
                if b.tophash[i+1] != emptyRest {
                    goto notLast
                }
            }
             // 当前值之后不存在其他的数据,则置为emptyRest
             // 需要把该位置之前除非空之外的所有emptyOne位置也置为emptyRest
            for {
                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
                    for b = bOrig; b.overflow(t) != c; b = b.overflow(t) {
                    }
                    i = bucketCnt - 1
                } else {
                    i--
                }
                if b.tophash[i] != emptyOne {
                    break
                }
            }
        notLast:
            h.count--
            break search
        }
    }
    // 抛出并发修改异常
    if h.flags&hashWriting == 0 {
        throw("concurrent map writes")
    }
    h.flags &^= hashWriting
}

删除数据的逻辑不是很复杂,但是里面对tophash中的状态进行了置换,增加了阅读代码的复杂度。这里主要是emptyOne和emptyRest这两个状态的转化,里面有几个临界点,下面一一说明:

  1. 当前删除元素是tophash中的最后一个元素,则需要检查它的下一个溢出桶tophash的第一个元素是否是emptyRest。如果不是,则直接return。
  2. 当前删除元素不是tophash中的最后一个元素,则检查当前tophash的下一个元素是否是emptyRest即可。如果不是,则直接return。
  3. 前两个条件都不满足的情况下,则需要将当前位置置为emptyRest,同时还要检测它前面的emptyOne状态的元素,将其均置为emptyRest,如下图所示:
empty

扩容

扩容的触发点在mapassign这个过程中,它主要检测了两个阈值:

  1. 达到了负载因子的上限,数量超过了6.5乘以bucket_num。这时候说明大部分的桶可能都快满了,如果插入新元素,有大概率需要挂在 overflow 的桶上。
  2. 溢出桶的数量过多。例如频繁的删除操作,导致溢出桶中很多位置都是emptyOne
func hashGrow(t *maptype, h *hmap) {
    bigger := uint8(1)
    if !overLoadFactor(h.count+1, h.B) {
        // 这里表示是溢出桶数量数量过多,但是map中元素数量还达不到过载,则无需增长桶数量
        // 分配一个与之前一样的即可,将所有数据摊平,减少溢出桶的数量
        bigger = 0
        h.flags |= sameSizeGrow
    }
    oldbuckets := h.buckets
    newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

    flags := h.flags &^ (iterator | oldIterator)
    if h.flags&iterator != 0 {
        flags |= oldIterator
    }
    // commit the grow (atomic wrt gc)
    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")
        }
        h.extra.oldoverflow = h.extra.overflow
        h.extra.overflow = nil
    }
    if nextOverflow != nil {
        if h.extra == nil {
            h.extra = new(mapextra)
        }
        h.extra.nextOverflow = nextOverflow
    }
    // 实际的拷贝并不是在此处进行,而是在growWork() and evacuate()时自动进行的。
}

在上面进行扩容标记,即将oldbuckets赋值之后,在mapdeletemapassign时都会调用到growWork这个方法,也就是扩容值拷贝实际上的入口。

func growWork(t *maptype, h *hmap, bucket uintptr) {
    // 要确保我们迁移的桶和当前用的桶是一致的
    evacuate(t, h, bucket&h.oldbucketmask())

    // 进行辅助迁移,当前桶迁移完成查看map是否还在扩容过程中,并帮助进行迁移
    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) {
        // 这里分为X区和Y取,即低位区和高位区,扩容之后桶容量翻倍,需要把原有数据
        // 均匀的分散在这两个区域
        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.v = add(x.k, bucketCnt*uintptr(t.keysize))

        if !h.sameSizeGrow() {
            // 仅仅在map容量翻倍之后再计算Y区的地址,否则GC会看到一个不存在的指针。
            y := &xy[1]
            y.b = (*bmap)(add(h.buckets, (oldbucket+newbit)*uintptr(t.bucketsize)))
            y.k = add(unsafe.Pointer(y.b), dataOffset)
            y.v = add(y.k, bucketCnt*uintptr(t.keysize))
        }

        for ; b != nil; b = b.overflow(t) {
            k := add(unsafe.Pointer(b), dataOffset)
            v := add(k, bucketCnt*uintptr(t.keysize))
            for i := 0; i < bucketCnt; i, k, v = i+1, add(k, uintptr(t.keysize)), add(v, uintptr(t.valuesize)) {
                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/value to bucket x or bucket y).
                    hash := t.key.alg.hash(k2, uintptr(h.hash0))
                    if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.alg.equal(k2, k2) { 

                        // key != key,只有在 float 数的 NaN 时会出现
                        // 比如:
                        // n1 := math.NaN()
                        // n2 := math.NaN()
                        // 这种情况下 n1 和 n2 的哈希值也完全不一样,
                        // 同时 tophash 对于 NaN 也没啥意义
                        // 所以对于这种 key 我们也可以随意对其目标进行发配
                        // 还是按正常的情况下算一个随机的 tophash
                        // 然后公平地把这些 key 平均分布到各 bucket 就好
                        useY = top & 1 // 保证这个key有一半的几率落到Y区,随机发配
                        top = tophash(hash)
                    } else {
                        // 例如newbit=8,其二进制即1000,如果hash&1000!=0,则第四位必须不为0
                        // 即xxxx1xxx,则在翻倍扩容后,桶位置肯定是oldsize+hash
                        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.v = 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 value
                }
                if t.indirectvalue() {
                    *(*unsafe.Pointer)(dst.v) = *(*unsafe.Pointer)(v)
                } else {
                    typedmemmove(t.elem, dst.v, v)
                }
                dst.i++
                // These updates might push these pointers past the end of the
                // key or value 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.v = add(dst.v, uintptr(t.valuesize))
            }
        }
        // Unlink the overflow buckets & clear key/value to help GC.
        if h.flags&oldIterator == 0 && t.bucket.kind&kindNoPointers == 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)
    }
}

func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
    h.nevacuate++
    // Experiments suggest that 1024 is overkill by at least an order of magnitude.
    // Put it in there as a safeguard anyway, to ensure O(1) behavior.
    stop := h.nevacuate + 1024
    if stop > newbit {
        stop = newbit
    }
    for h.nevacuate != stop && bucketEvacuated(t, h, h.nevacuate) {
        // 判断当前bucket的tophash的第一个元素是否已经被迁移,如果已经迁移,则自增
        h.nevacuate++
    }
    if h.nevacuate == newbit { // newbit == # of oldbuckets
        // 所有都已经迁移完毕,则将oldbuckets置空
        h.oldbuckets = nil
        // 丢弃掉老的溢出桶,如果它们还在被迭代器引用,此时迭代器只是引用一个切片的指针
        if h.extra != nil {
            h.extra.oldoverflow = nil
        }
        
        h.flags &^= sameSizeGrow
    }
}

在做数据迁移时,关注三个点即可:

  1. 对于那些相同的key在多次hash计算时得出的值不一样的情况下,随机找个bucket进行置放即可。
  2. 将桶中的其他原有数据通过合理的计算散布在新桶的高低位区。它这里取巧了一点,通过计算hash&newbit != 0,则证明该数据应该被分配到高位区,具体的解释在上面代码的注释上有。
  3. 当前桶已经迁移完成,判断是否需要进行辅助其他桶的迁移。

总结

在增加、查询、删除这几个操作中,其实有很多共性的地方,比如都需要先定位桶的位置,然后通过其hash值的高8位去tophash中查询是否有与此值匹配的位置存在,查询不到,则去overflow溢出桶的tophash中继续查询即可。只不过在其中做了很多逻辑判断和业务上的一些优化以及扩缩容处理,这样让代码看起来你略显复杂,咱们将问题拆解,再一步步去看,便会简单许多。从上面的分析也可以很清楚的了解到,go-map完全采用的是数组+链表的数据结构,数据量增大时,也并没有和其他语言一样将链表转换成树结构。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容