golang map源码浅析

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了golang map源码浅析相关的知识,希望对你有一定的参考价值。

参考技术A

golang 中 map的实现结构为: 哈希表 + 链表。 其中链表,作用是当发生hash冲突时,拉链法生成的结点。

可以看到, []bmap 是一个hash table, 每一个 bmap是我们常说的“桶”。 经过hash 函数计算出来相同的hash值, 放到相同的桶中。 一个 bmap中可以存放 8个 元素, 如果多出8个,则生成新的结点,尾接到队尾。

以上是只是静态文件 src/runtime/map.go 中的定义。 实际上编译期间会给它加料 ,动态地创建一个新的结构:

上图就是 bmap的内存模型, HOB Hash 指的就是 top hash。 注意到 key 和 value 是各自放在一起的,并不是 key/value/key/value/... 这样的形式。源码里说明这样的好处是在某些情况下可以省略掉 padding 字段,节省内存空间。

每个 bmap设计成 最多只能放 8 个 key-value 对 ,如果有第 9 个 key-value 落入当前的 bmap,那就需要再构建一个 bmap,通过 overflow 指针连接起来。

map创建方法:

我们实际上是通过调用的 makemap ,来创建map的。实际工作只是初始化了hmap中的各种字段,如:设置B的大小, 设置hash 种子 hash 0.

注意 :

makemap 返回是*hmap 指针, 即 map 是引用对象, 对map的操作会影响到结构体内部

使用方式

对应的是下面两种方法

map的key的类型,实现了自己的hash 方式。每种类型实现hash函数方式不一样。

key 经过哈希计算后得到hash值,共 64 个 bit 位。 其中后B 个bit位置, 用来定位当前元素落在哪一个桶里, 高8个bit 为当前 hash 值的top hash。 实际上定位key的过程是一个双重循环的过程, 外层循环遍历 所有的overflow, 内层循环遍历 当前bmap 中的 8个元素

举例说明: 如果当前 B 的值为 5, 那么buckets 的长度 为 2^5 = 32。假设有个key 经过hash函数计算后,得到的hash结果为:

外层遍历bucket 中的链表

内层循环遍历 bmap中的8个 cell

建议先不看此部分内容,看完后续 修改 map中元素 -> 扩容 操作后 再回头看此部分内容。

扩容前的数据:

等量扩容后的数据:

等量扩容后,查找方式和原本相同, 不多做赘述。

两倍扩容后的数据

两倍扩容后,oldbuckets 的元素,可能被分配成了两部分。查找顺序如下:

此处只分析 mapaccess1 ,。 mapaccess2 相比 mapaccess1 多添加了是否找到的bool值, 有兴趣可自行看一下。

使用方式:

步骤如下:

扩容条件 :

扩容的标识 : h.oldbuckets != nil

假设当前定位到了新的buckets的3号桶中,首先会判断oldbuckets中的对应的桶有没有被搬迁过。 如果搬迁过了,不需要看原来的桶了,直接遍历新的buckets的3号桶。

扩容前:

等量扩容结果

双倍扩容会将old buckets上的元素分配到x, y两个部key & 1 << B == 0 分配到x部分,key & 1 << B == 1 分配到y部分

注意: 当前只对双倍扩容描述, 等量扩容只是重新填充了一下元素, 相对位置没有改变。

假设当前map 的B == 5,原本元素经过hash函数计算的 hash 值为:

因为双倍扩容之后 B = B + 1,此时B == 6。key & 1 << B == 1, 即 当前元素rehash到高位,新buckets中 y 部分. 否则 key & 1 << B == 0 则rehash到低位,即x 部分。

使用方式:

可以看到,每一遍历生成迭代器的时候,会随机选取一个bucket 以及 一个cell开始。 从前往后遍历,再次遍历到起始位置时,遍历完成。

https://www.qcrao.com/2019/05/22/dive-into-go-map/

https://draveness.me/golang/docs/part2-foundation/ch03-datastructure/golang-hashmap/

https://www.bilibili.com/video/BV1Q4411W7MR?spm_id_from=333.337.search-card.all.click

[原创]深度剖析Golang map源码

一. 背景

在Go语言的学习和使用过程中,想必大家都会经常使用map数据结构吧,如果仅仅会使用,那么远远是不够的。今天就与大家从源码的角度,深入剖析一下Golang中map的实现。本文从map创建、插入、查询、删除、遍历这几个核心场景来学习。

涉及内容比较多,如果不正确的地方欢迎指正

二、前置知识

在学习map源码需要对Golang中非类型安全指针unsafe有一定了解,源码实现中大量使用unsafe.Pointer指针。

另外需要对内存对齐有一定了解。

三、问题引入

在学习前,我们先思考几个问题,方便我们后续带着这些疑问更好的进行学习


1. map是线程安全的吗?如果并发读写会出现什么情况


示例:

两开个协程对同一个map进行并发读写,执行发现报错了,且无法使用recover方法进行异常恢复

func main() {
    m := make(map[int64]int32,10)
    go func() {
        for {
            _ =m[2]
        }
    }()

    go func() {
        for {
            m[2]=3
        }
    }()
    select {

    }
}
/*
fatal error: concurrent map read and map write

goroutine 5 [running]:
runtime.throw(0x108b9f4, 0x21)
        /usr/local/go/src/runtime/panic.go:1117 +0x72 fp=0xc00004a780 sp=0xc00004a750 pc=0x102efb2
runtime.mapaccess1_fast64(0x1079fe0, 0xc000076060, 0x2, 0xc00007a0b8)
        /usr/local/go/src/runtime/map_fast64.go:21 +0x198 fp=0xc00004a7a8 sp=0xc00004a780 pc=0x100ead8
main.main.func1(0xc000076060)
        /Users/test3_3/main.go:20 +0x45 fp=0xc00004a7d8 sp=0xc00004a7a8 pc=0x106f9a5
runtime.goexit()
        /usr/local/go/src/runtime/asm_amd64.s:1371 +0x1 fp=0xc00004a7e0 sp=0xc00004a7d8 pc=0x105dcc1
created by main.main
Process finished with exit code 2
*/



2.在map遍历的时候不能依赖map遍历的顺序这是为什么?


示例:

运行示例,我们会发现两次for循环的输出是不一样的,如果一样请多试几次,或增大map中的元素个数

func Test1(t *testing.T) {
    myMap := make(map[int]int)
    for i := 0; i < 3; i++ {
        myMap[i]=i
    }
    for key, val := range myMap {
        fmt.Println(key,val)
    }
    fmt.Println("------")
    for key, val := range myMap {
        fmt.Println(key,val)
    }
}
/*
2 2
0 0
1 1
------
0 0
1 1
2 2
*/


3. new一个map和make一个map是等价的吗?为什么?

示例:运行以下代码我们发现make出来的map是可以正常add元素的,但是往new出来map中添加元素的时候出现了panic,所以结论new和make一个map是不等价的

func Test2(t *testing.T) {
    myMap1 := make(map[int]int)
    myMap2 := new(map[int]int)
    myMap1[1]=1 //ok
    (*myMap2)[1]=1 //panic: assignment to entry in nil map [recovered]
}


4.从map中delete一个key的进候,对应的val会进行内存释放吗?


5.map是使用hash表来实现的,如果出现了hash冲突,如何时行解决的?


四、概念介绍


桶(bucket)

用于真实存储Go map中的KV键值对的容器,一个桶中存储8对KV元素对。(对应bmap结构)


哈希桶(buckets)

是由一组bucket构成的数组(hmap.buckets),是一块连续的内存结构。

当往map中添加元素的时候,会根据key的hash值来确定的放到桶链中的一个桶中。


溢出桶(overflow bucket)

使用hash表来实现map,必须要面对hash冲突的情形。如果由于hash不均匀或其它情况导致很多个key都对应到了同一个hash桶,那么当某一个hash桶被装满的时候,就需要额外扩展出一个桶用于解决hash冲突

桶链(bucket chan)

是一个以哈希桶为节点头,0个或多个溢出桶构成的单向链表。

这里可以类比一下java中hashmap的实现,原理是一样的都是数组+链表来进行之现的,桶链其实就是用来解决hash冲突的


旧哈希桶(oldbuckets)

map在不断添加元素的过程中,可能会触发扩容,如果触发了扩容,就会申请一个新的哈希桶,那么原来的哈希桶就会被称之为旧哈希桶。即旧哈希桶仅在hash扩容时才有意义


map基础结构如下,基本可以看成是数组+链表的实现


五、源码分析


1.数据结构


源码文件见:src/runtime/map.go


hmap结构

 map底层数据结构,一个map底层对应一个hmap。


type hmap struct {
    count     int //map中kv元素个数,等价于len()
    /*
    iterator     = 1 // 可能正在遍历buckets
    oldIterator  = 2 // 可能正在遍历oldbuckets
    hashWriting  = 4 // 一个goroutine正在写当前map
    sameSizeGrow = 8 // 当前map正在进行等长扩容
    */

    flags     uint8  //map的状态,二制制表示
    B         uint8  // 值为log2(buckets),总共可容纳loadFactor * 2^B个元素,bucketSize = 1 << B
    noverflow uint16 // 近似的已经使用的溢出桶的个数
    hash0     uint32 // hash种子,确保hash函数更加分散

    buckets    unsafe.Pointer // []Buckets数组指针,即哈希桶。Buckets的长度为2^B,如果map的长度是0,那么这里是nil
    oldbuckets unsafe.Pointer // 扩容前的[]Buckets数组指针(扩容时用于复制的数组),如果该字段是非nil,那么说明正在扩容
    nevacuate  uintptr        // 扩容时迁移到新[]Buckets的buckets数

    extra *mapextra // 可选字段-存储溢出桶相关信息
}


hmap.mapextra结构



bmap(bucket桶)

内存示意图

[原创]深度剖析Golang map源码


在src/runtime/map.go中定义如下

type bmap struct {
        //每个key哈希值的高8位
    tophash [8]uint8
}

但是该函数会在编译的时候进行处理(处理方法src/cmd/compile/internal/gc/reflect.go中的bmap方法),增加了一些字段

最终定义如下

type bmap struct {
    topbits  [8]uint8      //keyhash的高8位
    keys     [8]KEY_TYPE   //存储key  如果key类型长度超128,就使用指针[8]*KEY_TYPE 
    elems    [8]VAL_TYPE   //存储value 如果value类型长度超128,就使用指针[8]*KEY_TYPE 
    overflow uintptr //指针  指向下一个桶(溢出桶)
}


经过分析,我们发现bmap结构中,是8个key后面紧跟了8个val,即

key1,key2,key3...key8,val1,val2,val3....val8,按照我们的理解,如果存储成

key1,val1.....key8,val8来说更容易操作和理解,但为什么要进行连续key和连续val存储的实现呢?


答案是这样的实现方式在某些情况下,可以减少内存对齐带来的内存浪费。两个struct,即便存储的数据一致,但由于字段摆放顺序不同,可能会由于内存对齐导致struct的大小不同。内存对齐计算的计算,这里不做特殊说明。


我们写个demo来验证下,比如map[int64]int8对应的bmap

如果采用keys,vals集中存储,对比key、val成对存储,一个bmap可以节省56字节

func Test1(t *testing.T) {
    t.Log(unsafe.Sizeof(bmap1{}))//88
    t.Log(unsafe.Sizeof(bmap2{}))//144
}

/*
KV集中存储
 */

type bmap1 struct {
    topbits  [8]uint8
    keys     [8]int64
    elems    [8]int8
    overflow uintptr
}

/*
KV成对存储
 */

type bmap2 struct {
    topbits  [8]uint8
    key0     int64
    elem0    int8
    key1     int64
    elem1    int8
    key2     int64
    elem2    int8
    key3     int64
    elem3    int8
    key4     int64
    elem4    int8
    key5     int64
    elem5    int8
    key6     int64
    elem6    int8
    key7     int64
    elem7    int8
    overflow uintptr
}



我们再来看下reflect.go中的bmap方法是如何修改bmap类型结构的字段的

func bmap(t *types.Type) *types.Type {

    bucket := types.New(TSTRUCT)
    keytype := t.Key()
    elemtype := t.Elem()

    ....
    ....

    //如果大于128key使用指针类型
    if keytype.Width > MAXKEYSIZE {
        keytype = types.NewPtr(keytype)
    }
    //如果大于128value使用指针类型
    if elemtype.Width > MAXELEMSIZE {
        elemtype = types.NewPtr(elemtype)
    }

    field := make([]*types.Field, 05)

    //新增topbits字段,用于保存高8位hash值
    arr := types.NewArray(types.Types[TUINT8], BUCKETSIZE)
    field = append(field, makefield("topbits", arr))

    //新增keys字段,用于保存keys
    arr = types.NewArray(keytype, BUCKETSIZE)
    arr.SetNoalg(true)
    keys := makefield("keys", arr)
    field = append(field, keys)

    //新增elems字段,用于保存values
    arr = types.NewArray(elemtype, BUCKETSIZE)
    arr.SetNoalg(true)
    elems := makefield("elems", arr)
    field = append(field, elems)

    /*新增overflow字段,是一个指针
    当桶满了的时候,下挂一个溢出桶
    */

    otyp := types.NewPtr(bucket)
    if !elemtype.HasPointers() && !keytype.HasPointers() {
        otyp = types.Types[TUINTPTR]
    }
    overflow := makefield("overflow", otyp)
    field = append(field, overflow)

    ....
    ....

    t.MapType().Bucket = bucket
    bucket.StructType().Map = t
    return bucket
}



2.核心方法


我们先看一下,一些涉及位运算的核心方法,这些方法在map的插入,查找,扩容等时都会有到


/*
将1进行二进位左移b位,高位丢弃,低位补0
等价于1<<b
*/

func bucketShift(b uint8) uintptr {
    return uintptr(1) << (b & (sys.PtrSize*8 - 1))
}

/*
求b的对应shfit值的的掩码,即bucketShift(b) - 1
*/

func bucketMask(b uint8) uintptr {
    return bucketShift(b) - 1
}

/*
求一个hash值的高8倍
因为hash值高8位,也用于一些特殊状态的记录,这些特殊状态值<5
如果一个key的hash值高8位确实是小于(minTopHash)5的,那么就加一个(minTopHash)5,避免hash值高8位等于一个特殊的状态值
*/

func tophash(hash uintptr) uint8 {
    top := uint8(hash >> (sys.PtrSize*8 - 8))
    if top < minTopHash {
        top += minTopHash
    }
    return top
}

/*
通过tophash[0]的状态值,来判断是否被迁移完成
如果是扩容迁移中,那么会在bmap的第一个tophash写入一个特殊的状态

以下是几个tophash[0]的特殊状态
//当前cell为空,且后续cell都为空,在遍历时会用到,加快map遍历速度
emptyRest      = 0
//当前cell为空
emptyOne       = 1 
//翻倍扩容时会用到,指示cell迁移到新bucket的前半部分
evacuatedX     = 2 
//翻倍扩容时会用到,指示cell迁移到新bucket的后半部分
evacuatedY     = 3 
//扩容时会用到。表示当前cell为空,且bucket已经迁移完成
evacuatedEmpty = 4 
//tophash的最小值,小于这个值,表示一个特殊状态
minTopHash     = 5 
*/

func evacuated(b *bmap) bool {
   h := b.tophash[0]
   return h > emptyOne && h < minTopHash
}

//在bmap里跳过topbit内存空间,找到key,value的起始偏移量
dataOffset = unsafe.Offsetof(struct {
    b bmap
    v int64
}{}.v)

//找到key、val存储空间的起始地址
func (b *bmap) keys() unsafe.Pointer {
    return add(unsafe.Pointer(b), dataOffset)
}


下面列常一些示例



B值
bucketShift bucketMask
0
00000001,1 00000000,0
1 00000010,2 00000001,1
2 00000100,4 00000011,3
3 00001000,8 00000111,7
4 00010000,16 00001111,15
5 00100000,32 00011111,31
6 01000000,64 00111111,63
7 10000000,128 01111111,127

3.Hash方法



hash方法的签名是,对应maptype方法里的hasher字段


func(unsafe.Pointer, uintptr) uintptr/* 入参1:unsafe.Pointer 需要求hash的值 入参2:uintptr hash种子  返回1: hash值*/



[原创]深度剖析Golang map源码


根据不同的类型调用是不同的hash方法实现,具体见下面

src/cmd/compile/internal/gc/reflect.go:dtypesym

src/cmd/compile/internal/gc/alg.go:genhash

//生成maptype
func dtypesym(t *types.Type) *obj.LSym {
switch t.Etype {
    case TMAP:
        s1 := dtypesym(t.Key())
        s2 := dtypesym(t.Elem())
        s3 := dtypesym(bmap(t))
        hasher := genhash(t.Key()) //这里确定hash函数

        ot = dcommontype(lsym, t)
        ot = dsymptr(lsym, ot, s1, 0)
        ot = dsymptr(lsym, ot, s2, 0)
        ot = dsymptr(lsym, ot, s3, 0)
        ot = dsymptr(lsym, ot, hasher, 0)
}
}

/*
根据不同的类型对应不同的hash方法
*/

func genhash(t *types.Type) *obj.LSym {
    switch algtype(t) {
    default:
        Fatalf("genhash %v", t)
    case AMEM0:
        return sysClosure("memhash0")
    case AMEM8:
        return sysClosure("memhash8")
    case AMEM16:
        return sysClosure("memhash16")
    case AMEM32:
        return sysClosure("memhash32")
    case AMEM64:
        return sysClosure("memhash64")
    case AMEM128:
        return sysClosure("memhash128")
    case ASTRING:
        return sysClosure("strhash")
    case AINTER:
        return sysClosure("interhash")
    case ANILINTER:
        return sysClosure("nilinterhash")
    case AFLOAT32:
        return sysClosure("f32hash")
    case AFLOAT64:
        return sysClosure("f64hash")
    case ACPLX64:
        return sysClosure("c64hash")
    case ACPLX128:
        return sysClosure("c128hash")
    //.....
    //.....
    return closure
}



一个key的hash值如下,如果在64位机器上,那么哈西值有64位,前8位称之为tophash,后续会存储在bmap中的tophash中,在查找等场景会使用到。

[原创]深度剖析Golang map源码



3.创建map


调用内置make方法来创建一个map,make map其实是一个语法糖,最终会调用go/src/runtime/map.go:makemap方法。但是new出一个map, 我们会出现是无法通过编译的


我们可以通过下例验证

/*
生成伪汇编代码
go tool compile -S -N -l main.go>main.txt
 */

func main() {
    _ = make(map[int64]int8,999)
}


生成的汇编代三我们可以看到,确实是调用的makemap方法

[原创]深度剖析Golang map源码


使用new创建map,仅仅是开辟了一个指针的空间,并没有调用makemap来初始化,实际上hmap还是nil,那么在set的时候就会panic,在后面看mapassign的时候大家会注意到


func main() {
    mapPtr := new(map[int]int)
    (*mapPtr)[2]=2 //赋值时panic: assignment to entry in nil map
}


接下来详细看下makemap方法


/*
t:存储了map相关的类型信息,比如key的类型信息,value的类型信息等, 具体见reflect.go:dtypesym方法
hint:map的预期大小,即make创建map时,第二个参数的大小,比如make(map[int64]int,999),那么hint大小为999
h:如果bucket可以在栈上创建,那么h不等于nil,如果需要在堆上创建,那么h不等于nil,
*/

func makemap(t *maptype, hint int, h *hmap) *hmap {
    /*
    溢出检查,如果hint太大,则将hint置为0
    */

    mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)
    if overflow || mem > maxAlloc {
        hint = 0
    }

    /*
    如果需要在堆上创建map,那么入参的h为nil,那么这里开避一个空间
    */

    if h == nil {
        h = new(hmap)
    }
    //随机生成hash种子
    h.hash0 = fastrand()

    /*
    根据预期的hint map大小来计算B值
    B值默认为0
    如果hint>8或hint>哈希桶大小*负载因子(6.5),那么B++,即容量需要扩大2倍
    */

    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.
    /*
    如果B等于0,那么不创建哈希桶,在插入的时候进行懒加载
    如果B等于0,那么创建对应的哈希桶和溢出桶
    其具一会看makeBucketArray方法
    */

    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 指向可用的溢出桶
            h.extra.nextOverflow = nextOverflow
        }
    }

    return h
}



/*
上面已经进行了解释,某一个B值对应的map容量的的81.25%(6.5/8,6.5是负载因子,8代表每一个bmap桶可以容纳8个KV对)
是否超过了count,如果超过,返回true
*/

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


makeBucketArray根据B值来创建哈希桶和溢出桶,都是非必要的,可能是懒创建哈希桶,也可能因为哈希桶大小不够大,而无需创建溢出桶。

有一点需要注意,就是哈希桶(buckets)和溢出桶(overflow)是一块连续的内存空间,这样可以把随机存储转变为顺序存储,提升性能。(这就是为啥哈希桶和溢出桶要使用一块连续内存空间的原因)

func makeBucketArray(t *maptype, b uint8, dirtyalloc unsafe.Pointer) (buckets unsafe.Pointer, nextOverflow *bmap) {
    //计算b对应的掩码,对b对应的哈希桶个数
    base := bucketShift(b)
    //实际需要创建的桶个数,等于哈希桶个数+溢出桶个数
    nbuckets := base
    //如果B小于4是不需要创建溢出桶的
    if b >= 4 {
        //B值掩码右移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 {
        buckets = dirtyalloc
        size := t.bucket.size * nbuckets
        if t.bucket.ptrdata != 0 {
            memclrHasPointers(buckets, size)
        } else {
            memclrNoHeapPointers(buckets, size)
        }
    }

    /*
    base != nbuckets 说明是有溢出桶被创建的
    */

    if base != nbuckets {
        //获取第一个溢出桶的位置
        nextOverflow = (*bmap)(add(buckets, base*uintptr(t.bucketsize)))
        //获取最后一个溢出桶的位置
        last := (*bmap)(add(buckets, (nbuckets-1)*uintptr(t.bucketsize)))
        //让最后一个溢出桶的overflow字段指向第一个哈希桶
//主要作用是标识了这是最后一个溢出桶!!!
     last.setoverflow(t, (*bmap)(buckets))    }
    
return buckets, nextOverflow
}


map创建的过程基本就完成了,下面看一个示例。

创建一个map,make(map[int64]int8,999)后的内存布局

[原创]深度剖析Golang map源码





3.写入(put)


map写入时,其实是调用mapassign方法,查看汇编代码

func main() {
    mymap := make(map[struct{}]int8,999)
    mymap[struct{}{}]=2
}


[原创]深度剖析Golang map源码


下面我们来分析mapassign方法

mapassign是往map赋值的时候会调用的,该方法的主要目地有两个

a.在hash桶中找到目标桶

b.在目标桶中找到对应存储位cell。

整体过程是如下:

1.如果set的时候,发现当前key是存在的那么就直接重写这个cell的val值;

2.如果有空cell,就使用第一个空cell;

3.如果cell全满足了,则进行扩容一个溢出桶,放在第一个cell中。


大概流程图如下

[原创]深度剖析Golang map源码

为什么在查找目标cell的时候要优先比较hash值的高8位呢?

因为hash值的低位在确定目标桶时已经使用了,使用高8位可以尽可能充分利用key的哈希值。


/*
入参:
t:map类型信息
h:map内存结构
key:key对应指针

返回:value的存储地址的指针
*/

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
    /*
    如果给一个nil map写值,那么会报一个panic
    这就是为啥给一个没有被make的值为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)
    }
    /*
    并发校验,
    如果写map,会将hmap.flags中打上hashWriting的标记,
    如果flags中有这个标记,表示其它协程正在写这个map,此时抛出异常
    throw出一个无法恢复的panic异常,退出码为2
    */

    if h.flags&hashWriting != 0 {
        throw("concurrent map writes")
    }

    //使用哈希种子和key,调用hasher方法求key的哈希值
    hash := t.hasher(key, uintptr(h.hash0))

    /*
    在hmap的flags中记录 hashWriting 这个状态码
    */

    h.flags ^= hashWriting

    /*
    这里和makemap方法对应上了
    如果hmap.B值为0的时候,会将哈希桶进行懒加载的
    那么开辟空间,创建一个bucket
    等价于newarray(t.bucket, 1)
    */

    if h.buckets == nil {
        h.buckets = newobject(t.bucket)
    }

again:
    /*
    根据key的hash值来确定目标桶
    key的hash值与bucketMask(h.B)按位与
    这里和java HashMap原理一样,使用位运算代替取余运算,提高速度
    */

    bucket := hash & bucketMask(h.B)
    /*
    校验是map是否处于正在扩容中
    */

    if h.growing() {
        /*
        如果map正在处于扩容中
        那么首先将oldbuckets对应的节点迁移到新的bucket中
        迁移完成后再存储当前的KV
        */

        growWork(t, h, bucket)
    }
    //根据内存偏移量,找到目标桶
    b := (*bmap)(add(h.buckets, bucket*uintptr(t.bucketsize)))
    /*
    计算hash值的高8位
    用于后续的数据查找
    */

    top := tophash(hash)

    //目标桶的目标cell对应的tophash的地址
    var inserti *uint8
    //目标桶中存储Key对应的指针
    var insertk unsafe.Pointer
    //目标桶中存储Value对应的指针
    var elem unsafe.Pointer
bucketloop:
    /*
    这里有两层循环
    第一层是遍历桶链
    */

    for {
        /*
        第二层 遍历bucket的8个cell
        */

        for i := uintptr(0); i < bucketCnt; i++ {
            /*
            优先比较hash值高8位
            为什么要比较高8位呢?因为hash的后几位参与了目标桶计算,高8位一般情况下用不到
            这里优化成使用高8位,则充分利用了hash值的每一位。
            */

            if b.tophash[i] != top {
                /*
                优先找到一个可插入的空位

                b.tophash[i] 的状态为
                emptyRest      = 0 当前cell为空且后续cell都为空
                emptyOne       = 1 当前cell为空
                那么这个KV就可以放到当前桶的第i个cell中了

                当前如果找到了这个空位,先记下来,一样需要继续循环,看一下相同的key是否存在
                实际的还在后面
                */

                if isEmpty(b.tophash[i]) && inserti == nil {
                    //注意这里是地址
                    inserti = &b.tophash[i]
                    /*
                    根据内存偏移量来计算bmap中存储第i个key的地址
                    从key开始的偏移量+keysize*i
                    */

                    insertk = add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
                    /*
                    根据内存偏移量来计算bmap中存储第i个value的地址
                    从key开始的偏移量+keysize*8 +i*elemsize
                    */

                    elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                }
                /*
                如果当前cell的tophash对应的状态为emptyRest ,说明这是最后一个cell了,已经遍历完成了
                */

                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不相等,说明产生了hash冲突
            表示当前cell不可用,继续寻找cell
            */

            if !t.key.equal(key, k) {
                continue
            }
            // already have a mapping for key. Update it.
            if t.needkeyupdate() {
                typedmemmove(t.key, k, key)
            }
            //这个就是目标cell存储val的对应指针
            elem = add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            goto done
        }
        /*
        继续查找溢出桶,即遍历桶链,继续找cell
        */

        ovf := b.overflow(t)
        if ovf == nil {
            break
        }
        b = ovf
    }

    /*
    在没有合适的cell的情况下,判断是否需要扩容

    有两个条件可以触发扩容
    1.当前map中元素的个数大于8且负载因子大小超过6.5(81.25%)
    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
    }
    /*
    遍历完桶链后,没有任何一个符合条件的cell
    那么扩展一个溢出桶,放到溢出桶里
    并拿到目标cell的topbit、key、value地址
    */

    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))
    }

    // 针对指针进行处理
    if t.indirectkey() {
        kmem := newobject(t.key)
        *(*unsafe.Pointer)(insertk) = kmem
        insertk = kmem
    }
    // 针对指针进行处理
    if t.indirectelem() {
        vmem := newobject(t.elem)
        *(*unsafe.Pointer)(elem) = vmem
    }
    typedmemmove(t.key, insertk, key)
    //将key的top hash值放到目标backet中的指定cell中
    *inserti = top
    //hmap的计数加一
    h.count++

done:
    //状态校验,正常情况应该是1,为0表示有并发操作
    if h.flags&hashWriting == 0 {
        throw("concurrent map writes")
    }

    /*
    回退掉hmap.flags中的hashWriting状态
    表示当前map没有写入
    */

    h.flags &^= hashWriting
    if t.indirectelem() {
        elem = *((*unsafe.Pointer)(elem))
    }
    //返回存储val的地址
    return elem
}


追加一个溢出桶的逻辑我们单独看下,主要是newoverflow方法


func (h *hmap) newoverflow(t *maptype, b *bmap) *bmap {
    var ovf *bmap
    /*
    如果有可用的溢出桶
    这里的溢出能在创建map的时候,可能已经预先创建好了
    */

    if h.extra != nil && h.extra.nextOverflow != nil {

        ovf = h.extra.nextOverflow
        /*
        当前溢出桶的overflow为nil
        说明还没有到最后一个可用的溢出桶,
        因为最后一个可用的溢出桶的overflow指向了第一个hash桶
        */

        if ovf.overflow(t) == nil {
            //指向下一个可用的溢出桶
            h.extra.nextOverflow = (*bmap)(add(unsafe.Pointer(ovf), uintptr(t.bucketsize)))
        } else {
            /*
            这是最后一个可用的溢出桶了
            最后一个溢出桶的原来指向第一个hash桶的指针改为nil
            */

            ovf.setoverflow(t, nil)
            //可用溢出桶没有了,那么置为nil
            h.extra.nextOverflow = nil
        }
    } else {
        //没有预先分配的移除桶,那么直接创建一个
        ovf = (*bmap)(newobject(t.bucket))
    }
    //增加hamp的 noverflow字段值
    h.incrnoverflow()
    // key和value 非指针时
    if t.bucket.ptrdata == 0 {
        h.createOverflow()
        //将hmap.extra.overflow中记录当前的溢出桶指针
        *h.extra.overflow = append(*h.extra.overflow, ovf)
    }
    //给入参的bmap的桶链末尾追加当前溢出桶
    b.setoverflow(t, ovf)
    return ovf
}

/*
增加hamp的 noverflow字段值 近似的记录了使用了多少个溢出桶

*/

func (h *hmap) incrnoverflow() {
    if h.B < 16 {
        //增加hamp中的益处桶个数记录
        h.noverflow++
        return
    }

    /*
    当桶过多的时候,根据随机数来判断h.noverflow是否需要加1
    这也就是之前说为什么noverflow字段是一个近似值。
    至于为啥,我也没想明白。。。。。谁知道可以call我一下
    */

    mask := uint32(1)<<(h.B-15) - 1

    if fastrand()&mask == 0 {
        h.noverflow++
    }
}



4.查找


查找根据不同的场景主要对应三个方法,

mapaccess1,是针对 val:=map["key"] 这种一个返回值的形式

mapaccess2,是针对 val,ok:=map["key"] 这种两个返回值的形式

mapaccessK,是针对遍历map时同时返回key、value的形式

func mapaccess1(t *maptype, h *hmap, key unsafe.Pointerunsafe.Pointer
func mapaccess2(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, bool)
func mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer)


三个方法实现基本一样,我们以mapaccessK为例进行阅读

主要逻辑就是通过2层循环,第一层是遍历桶链,第二层是遍历每个桶里的cell,再根据hash值高8位和key值来判断是否找到,还有一个特殊逻辑就是要处理某个key还没有被迁移的中间态。


还有一点注意下,扩容是渐进式的,仅仅在写入和删除时会触发(growWork的调用方),读取是不会触发的

func mapaccessK(t *maptype, h *hmap, key unsafe.Pointer) (unsafe.Pointer, unsafe.Pointer) {
    /*
    nil map的读取是不会报错的,就是这里体现的
    如果map为空,这里也返回nil
    */

    if h == nil || h.count == 0 {
        return nilnil
    }
    /*
    求key的hash值
    */

    hash := t.hasher(key, uintptr(h.hash0))
    //求掩码
    m := bucketMask(h.B)
    //根据位运算得到目标桶
    b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + (hash&m)*uintptr(t.bucketsize)))
    //如果oldbuckets不为空,说明正在扩容中
    if c := h.oldbuckets; c != nil {
        //如果是双倍扩容
        if !h.sameSizeGrow() {
            //掩码右移一位,得到扩容前的掩码
            m >>= 1
        }
        //在老桶中定位到对应位置
        oldb := (*bmap)(unsafe.Pointer(uintptr(c) + (hash&m)*uintptr(t.bucketsize)))
        /*
        根据老哈希桶的目标桶的tophash[0]的状态判断是否已经被迁移了。
        如果没有被迁移,则在里面寻找
        */

        if !evacuated(oldb) {
            //对应到老桶的位置
            b = oldb
        }
    }
    //取高8位
    top := tophash(hash)
    //遍历桶链
bucketloop:
    for ; b != nil; b = b.overflow(t) {
        for i := uintptr(0); i < bucketCnt; i++ {
            //如果hash高8位不同
            if b.tophash[i] != top {
                //并且当前cell后面都是空cell
                if b.tophash[i] == emptyRest {
                    //那么直接跳出循环,也就是遍历完成了也没有找到
                    break bucketloop
                }
                continue
            }
            //如果hash高8位相等,取当前cell对应的key值的位置
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            if t.indirectkey() {
                k = *((*unsafe.Pointer)(k))
            }
            //如果就是当前key,那么再找到val的位置返回
            if t.key.equal(key, k) {
                e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
                if t.indirectelem() {
                    e = *((*unsafe.Pointer)(e))
                }
                return k, e
            }
            //否则继续遍历下一个cell
        }
    }
    return nilnil
}


indirectelem的含义是特殊处理key/value是指针的情况,map会对大于128B的key和elem进行优化,在bmap中并不会直接存储对应的数据,而是会存指针,这样做的好处是在map初始化时可以不至于一次性申请过大的内存,而是将大内存的申请分配到每一次的写入操作中


5.删除


删除调用的对应方法是mapdelete

主要逻辑是将对应的cell的tophash置为emtyp或reset,

有一点需要注意,del一个元素只是更改了tophash的状态,在key、val是指针的情况释放val对应的内存


另外还有一个操作是当删除了key的时候,需要重置被删cell的状态为empty还是reset,还要连带处理当前cell之前的cell的状态


func mapdelete(t *maptype, h *hmap, key unsafe.Pointer) {
    if raceenabled && h != nil {
        callerpc := getcallerpc()
        pc := funcPC(mapdelete)
        racewritepc(unsafe.Pointer(h), callerpc, pc)
        raceReadObjectPC(t.key, key, callerpc, pc)
    }
    if msanenabled && h != nil {
        msanread(key, t.key.size)
    }
    /*
    针对nil map的删除操作是允许的,一个空操作罢了
    */

    if h == nil || h.count == 0 {
        if t.hashMightPanic() {
            t.hasher(key, 0
        }
        return
    }
    /*
    判断hmap的flags状态中是否有写状态,
    如果有表示在并发写,直接exit(2)
    */

    if h.flags&hashWriting != 0 {
        throw("concurrent map writes")
    }

    //求key的哈希值
    hash := t.hasher(key, uintptr(h.hash0))

    //更新hamp的状态为写ing状态
    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) {
        /*
        第二层遍历cell
        */

        for i := uintptr(0); i < bucketCnt; i++ {
            //hash高8位不等,那么肯定不是当前cell
            if b.tophash[i] != top {
                //如果当前cell后面全是空的,直接结束,没找到
                if b.tophash[i] == emptyRest {
                    break search
                }
                continue
            }
            //hash高8位相等
            k := add(unsafe.Pointer(b), dataOffset+i*uintptr(t.keysize))
            k2 := k
            if t.indirectkey() {
                k2 = *((*unsafe.Pointer)(k2))
            }
            //key不相等,继续下一个cell
            if !t.key.equal(key, k2) {
                continue
            }
            //如果key是指针,那么释放指针空间
            if t.indirectkey() {
                *(*unsafe.Pointer)(k) = nil
            } else if t.key.ptrdata != 0 {
                memclrHasPointers(k, t.key.size)
            }
            //找到val对应的空间
            e := add(unsafe.Pointer(b), dataOffset+bucketCnt*uintptr(t.keysize)+i*uintptr(t.elemsize))
            //如果val是指针,释放对应空间
            if t.indirectelem() {
                *(*unsafe.Pointer)(e) = nil
            } else if t.elem.ptrdata != 0 {
                memclrHasPointers(e, t.elem.size)
            } else {
                memclrNoHeapPointers(e, t.elem.size)
            }
            //因为当前cell要被删除所以cell状态置为空(emptyOne)
            b.tophash[i] = emptyOne
            //如果是最后一个cell并且还下挂了溢出桶,继续
            if i == bucketCnt-1 {
                if b.overflow(t) != nil && b.overflow(t).tophash[0] != emptyRest {
                    goto notLast
                }
            } else {
                //!(如果当前cell的下一个是emptyRest,即后面全是空)
                if b.tophash[i+1] != emptyRest {
                    goto notLast
                }
            }
            //循环调整该cell前的状态,如果需要的话,置为emptyRest
            for {
                //当前cell重置为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
                    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--
            // 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
        }
    }
    //如果状态有问题,exit2
    if h.flags&hashWriting == 0 {
        throw("concurrent map writes")
    }
    //重置hmap写状态
    h.flags &^= hashWriting
}




6.扩容


涉及到的相关方法:

方法名
说明
growing 是否是扩容中
overLoadFactor 是否超过了负载系数
tooManyOverflowBuckets 是否有过多的bucket
hashGrow 执行扩容前的相关字段的调整
growWork 渐进式迁移1-2个原节点
evacuate 真实的迁移


扩容方式:

a.翻倍扩容

--场景,超过了负载系数

b.等量扩容(存储分存调整)

--如果溢出桶超过了一定比例,说明存储比较稀疏,那么就进行等量扩容,来调整存储的分存,让其再加紧密。溢出桶也就是表示桶链过长,会降低查找的性能


扩容迁移注意点:

a.扩容迁移是随机的,没有顺序性,优先迁移当前新目标桶中对应的原哈希桶中的桶;其次迁移下一下,直到迁移完成.

b.如果翻倍扩容,原哈希桶中的某一个桶(等量扩容的时候,bucket index不变),只能落到新哈希桶中确定的两个桶中的其一,根据key的hash值中的第新桶的B+1位是0或1来判断,0-index不变,1-index=原index+原哈希桶长度,这个规律需要牢记,否则很难理解,具体这个见下面的例子




//根据hmap.oldbuckets是否为空,判断是否正在处于扩容中
func (h *hmap) growing() bool {
    return h.oldbuckets != nil
}

//map的实际值+1>8 并且 实际值+1已经超过加载因子
func overLoadFactor(count int, B uint8) bool {
    return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

//判断是否有过多的溢出桶
func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
    if B > 15 {
        B = 15
    }

    return noverflow >= uint16(1)<<(B&15)
}

//扩容时,相关字段的调整,并没有进行真实的迁移工作
func hashGrow(t *maptype, h *hmap) {
    bigger := uint8(1)
    /*
    如果没有超过负载系数,
    那么就是等量扩容
    */

    if !overLoadFactor(h.count+1, h.B) {
        bigger = 0
        //h.flags中记录等量扩容的状态
        h.flags |= sameSizeGrow
    }

    oldbuckets := h.buckets
    //创建一个新的桶,还有预先创建一些溢出桶
    newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)
    // 这块看的我很蒙逼。。。。暂时先参考了人家的解释。。先这样吧
    // A  &^ B, 与非操作, 移除 B 当中相应的标记位1
    // A  ^  B, 异或操作, 移除 A,B 共有的标记, 添加 A,B 不共有的标记
    // A  |  B, 或操作,   添加 B 当中相应的标记位1
    // A  &  B, 与操作,   寻找 A,B 共有的标记
    // 注意 &^ 运算符("与非"), 这块代码的逻辑是转移标志位(移除iterator, oldIterator). 
    flags := h.flags &^ (iterator | oldIterator)
    if h.flags&iterator != 0 {
        //当 old flags 存在iterator, 需要在new flags 当中添加 oldIterator
        flags |= oldIterator
    }

    h.B += bigger
    h.flags = flags
    //将原来的bucket放到oldbuckets
    h.oldbuckets = oldbuckets
    //新的bucket
    h.buckets = newbuckets
    //新map被迁移的数为0
    h.nevacuate = 0
    //使用的溢出桶个数重置为0
    h.noverflow = 0

    if h.extra != nil && h.extra.overflow != nil {
        if h.extra.oldoverflow != nil {
            throw("oldoverflow is not nil")
        }
        //原overflow转移到oldoverflow
        h.extra.oldoverflow = h.extra.overflow
        //新的overflow重置为nil
        h.extra.overflow = nil
    }
    if nextOverflow != nil {
        if h.extra == nil {
            h.extra = new(mapextra)
        }
        //重新设置可用的溢出桶
        h.extra.nextOverflow = nextOverflow
    }
}


func growWork(t *maptype, h *hmap, bucket uintptr) {
    /*
    迁移新桶对应的老桶到新桶
    */

    evacuate(t, h, bucket&h.oldbucketmask())

    //再迁移一个
    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)))
    /*
    获取扩容之前的桶数量
    如果是等量扩容,B值不变,如果是翻倍扩容,B值减一
    */

    newbit := h.noldbuckets()
    /*
    evacuatedX    
    evacuatedY    
    evacuatedEmpty
    根据桶的tophash[0]状态,判断桶是否已经被迁移完成
    如果当前桶没有迁移完成,就迁移
    */

    if !evacuated(b) {
        /*
        取旧桶,可能映射到新桶的2个桶
        x为新哈希桶的低位桶,即新目标桶的index等于旧桶的index
        */

        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() {
            /*
            y为新哈希桶的高位桶,即新目标桶的index等于旧桶的index+旧哈希桶长度
            */

            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) {
            //取当前桶的key起始地址
            k := add(unsafe.Pointer(b), dataOffset)
            //取当前桶的val起始地址
            e := add(k, bucketCnt*uintptr(t.keysize))
            //遍历cell
            for i := 0; i < bucketCnt; i, k, e = i+1, add(k, uintptr(t.keysize)), add(e, uintptr(t.elemsize)) {
                top := b.tophash[i]
                /*
                该cell为空,或该cell后续cell都为空,
                */

                if isEmpty(top) {
                    //该cell状态记录--当前cell为空,且该桶已经被迁移
                    b.tophash[i] = evacuatedEmpty
                    //继续遍历
                    continue
                }
                if top < minTopHash {
                    throw("bad map state")
                }
                k2 := k
                if t.indirectkey() {
                    k2 = *((*unsafe.Pointer)(k2))
                }
                //是否使用新哈希桶的高位桶,默认为0即false
                var useY uint8
                //非等量扩容
                if !h.sameSizeGrow() {
                    hash := t.hasher(k2, uintptr(h.hash0))
                    /*
                    针对math.NaN()这种的key做特殊处理。
                    因为math.NaN()的每次计算hash值都和原来的不一样,也就是哈希值会变。
                    所以在迁移的时候,只能再次求原来tophash的最低一位来决定是放到 x部分还是y部分
                    */

                    if h.flags&iterator != 0 && !t.reflexivekey() && !t.key.equal(k2, k2) {
                        useY = top & 1
                        top = tophash(hash)
                    } else {
                        /*
                        其它正常情况
                        判断原桶个数的二进制的最高位是否是1
                        如果是那么放到目标桶的高位桶,否则放到低位桶
                        */

                        if hash&newbit != 0 {
                            useY = 1
                        }
                    }
                }

                if evacuatedX+1 != evacuatedY || evacuatedX^1 != evacuatedY {
                    throw("bad evacuatedN")
                }
                //记录bmap中的迁移状态,是迁到高位桶还是低位桶
                b.tophash[i] = evacuatedX + useY 
                //目标桶的桶位被确定(x或y)
                dst := &xy[useY]                
                /*
                如果目标桶被紧密的排满了
                那么创建一个溢出桶
                并放到新创建溢出桶的第0个桶
                */

                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))
                }
                /*
                这里就是实际的迁移操作,
                tophash
                key和val
                */

                dst.b.tophash[dst.i&(bucketCnt-1)] = top 
                if t.indirectkey() {
                    *(*unsafe.Pointer)(dst.k) = k2 
                } else {
                    typedmemmove(t.key, dst.k, k) 
                }
                if t.indirectelem() {
                    *(*unsafe.Pointer)(dst.e) = *(*unsafe.Pointer)(e)
                } else {
                    typedmemmove(t.elem, dst.e, e)
                }
                //因为是紧密排列,所以i++移到下一个cell
                dst.i++

                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)
        }
    }
    /*
    更新迁移状态
    bucket迁移是随机迁移,没有顺序
    map在写入时会优先迁移当前正在写入的bucket,如果当前迁移的bucket是hmap中记录的待迁移的下一个bmap,则更新迁移进度。
    */

    if oldbucket == h.nevacuate {
        advanceEvacuationMark(h, t, newbit)
    }
}


/*
获取扩容之前的桶数量
如果是等量扩容,B值不变,如果是翻倍扩容,B值减一
*/

func (h *hmap) noldbuckets() uintptr {
    oldB := h.B
    if !h.sameSizeGrow() {
        oldB--
    }
    return bucketShift(oldB)
}

/*
根据桶的tophash[0]状态,判断桶是否已经被迁移完成
*/

func evacuated(b *bmap) bool {
    h := b.tophash[0]
    return h > emptyOne && h < minTopHash
}
//更新一下迁移进度
func advanceEvacuationMark(h *hmap, t *maptype, newbit uintptr) {
    //已迁移桶数数加1。这里没太明白,每次迁移都是两个,为啥只加1。有人知道理解了可能call我下
    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) {
        h.nevacuate++
    }
    //nevacuate == newbit, 表示所有的旧桶都已经搬迁完成
    if h.nevacuate == newbit { 
        //将oldbuckets置为nil
        h.oldbuckets = nil
        if h.extra != nil {
            //extra.oldoverflow 置为nil 进行状态恢复
            h.extra.oldoverflow = nil
        }
        //等量扩容标记位回退
        h.flags &^= sameSizeGrow
    }
}


7.遍历


遍历的时候,会随机选择开始的bucket和cell,之后再依次遍历。

map的扩容是渐进式的,如果处于扩容中的时候,需要遍历未迁移和已迁移的桶。


对应的实现为mapiterinit ,有兴趣的可以通过go tool compile进行查看


首先看一下hiter,它是一个hash迭代器,

hiter:见src/cmd/compile/internal/gc/reflect.go:hiter


下面看一下源码,大概就是使用hiter记录迭代的过程,直到迭代完成

//遍历
/*
hiter:见src/cmd/compile/internal/gc/reflect.go:hiter
*/

func mapiterinit(t *maptype, h *hmap, it *hiter) {
    //竞态检测 -race
    if raceenabled && h != nil {
        callerpc := getcallerpc()
        racereadpc(unsafe.Pointer(h), callerpc, funcPC(mapiterinit))
    }
    //遍历空map没有影响
    if h == nil || h.count == 0 {
        return
    }
    //hiter合法性校验
    if unsafe.Sizeof(hiter{})/sys.PtrSize != 12 {
        throw("hash_iter size incorrect"// see cmd/compile/internal/gc/reflect.go
    }
    it.t = t

    //获取hamp,即将遍历的map
    it.h = h

    //记录当前遍历时的桶状态
    it.B = h.B
    it.buckets = h.buckets
    if t.bucket.ptrdata == 0 {
        //在迭代器中记录溢出桶的相关状态
        h.createOverflow()
        it.overflow = h.extra.overflow
        it.oldoverflow = h.extra.oldoverflow
    }

    // 随机从一个桶开始,这就是不能依赖map遍历顺序的原因
    r := uintptr(fastrand())
    if h.B > 31-bucketCntBits {
        r += uintptr(fastrand()) << 31
    }
    //起始桶
    it.startBucket = r & bucketMask(h.B)
    it.offset = uint8(r >> h.B & (bucketCnt - 1))

    // iterator state
    it.bucket = it.startBucket

    // 可以并发执行多个迭代器
    if old := h.flags; old&(iterator|oldIterator) != iterator|oldIterator {
        atomic.Or8(&h.flags, iterator|oldIterator)
    }
    //具体迭代
    mapiternext(it)
}



func mapiternext(it *hiter) {
    h := it.h
    if raceenabled {
        callerpc := getcallerpc()
        racereadpc(unsafe.Pointer(h), callerpc, funcPC(mapiternext))
    }
    //迭代的过程有数据写入,那么exit2
    if h.flags&hashWriting != 0 {
        throw("concurrent map iteration and map write")
    }
    t := it.t
    bucket := it.bucket
    b := it.bptr
    i := it.i
    checkBucket := it.checkBucket

next:
    if b == nil {
        if bucket == it.startBucket && it.wrapped {
            // end of iteration
            it.key = nil
            it.elem = nil
            return
        }
        //针对扩容中的情况进行特殊处理
        if h.growing() && it.B == h.B {
            oldbucket := bucket & it.h.oldbucketmask()
            b = (*bmap)(add(h.oldbuckets, oldbucket*uintptr(t.bucketsize)))
            if !evacuated(b) {
                checkBucket = bucket
            } else {
                b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
                checkBucket = noCheck
            }
        } else {
            b = (*bmap)(add(it.buckets, bucket*uintptr(t.bucketsize)))
            checkBucket = noCheck
        }
        //取下一个桶
        bucket++ 
        if bucket == bucketShift(it.B) {
            bucket = 0
            it.wrapped = true
        }
        i = 0
    }
    //遍历桶中的cells
    for ; i < bucketCnt; i++ {
        offi := (i + it.offset) & (bucketCnt - 1)
        //如果cell为空,或者已经被迁移,就不处理这个cell
        if isEmpty(b.tophash[offi]) || b.tophash[offi] == evacuatedEmpty {
            continue
        }
        //获取key、val内存地址
        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() {


            if t.reflexivekey() || t.key.equal(k, k) {

                hash := t.hasher(k, uintptr(h.hash0))
                if hash&bucketMask(it.B) != checkBucket {
                    continue
                }
            } else {

                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)) {

            it.key = k
            if t.indirectelem() {
                e = *((*unsafe.Pointer)(e))
            }
            it.elem = e
        } else {

            rk, re := mapaccessK(t, h, k)
            if rk == nil {
                continue 
            }
            it.key = rk
            it.elem = re
        }
        it.bucket = bucket
        if it.bptr != b { 
            it.bptr = b
        }
        //cell 迭代器 加1 
        it.i = i + 1 
        it.checkBucket = checkBucket
        return
    }
    //遍历溢出桶
    b = b.overflow(t)
    i = 0
    goto next
}


以上是关于golang map源码浅析的主要内容,如果未能解决你的问题,请参考以下文章

.7-浅析webpack源码之WebpackOptionsDefaulter模块

Set(一):HashSet、LinkedHasSet源码浅析

.4-浅析webpack源码之convert-argv模块

浅析RxJava 1.x&2.x版本区别及原理:maplift操作符源码解析

flinkFlink 1.12.2 源码浅析 : StreamTask 浅析

kernel源码浅析