make(map[T1]T2) 会返回一个*hmap(因为返回的是一个指针,所以对map的改动不管是否触发了扩容都不需要重新赋值,而append slice后则需要重新赋值),其结构如下:

type hmap struct {
    // 元素数量
    count     int
    flags     uint8
    // 桶数量的以二为底的对数值
    B         uint8
    noverflow uint16
    // 哈希种子
    hash0     uint32

    buckets    unsafe.Pointer
    //在扩容时保存老的桶
    oldbuckets unsafe.Pointer
    nevacuate  uintptr

    extra *mapextra
}

type mapextra struct {
    // 溢出桶
    overflow    *[]*bmap
    oldoverflow *[]*bmap
    nextOverflow *bmap
}

// 桶的结构体
type bmap struct {
    tophash [bucketCnt]uint8
}

实际上bmap在编译时会被填入以下字段,这是因为最初golang不支持范型:

type bmap struct { 
    topbits  [8]uint8
    // 键值对
    keys     [8]keytype
    values   [8]valuetype
    // 对齐填充
    pad      uintptr
    // 指向下一个溢出桶的指针
    overflow uintptr
}

tophash是键的最高八位,当对键值对操作时,会首先对比tophash和目标对象的高八位,如果相等才会对比keys和目标对象,这样可以提高效率。因为选取桶序号是哈希的低几位,所以一个桶中不会有很多相等的tophash,这样也可以避免冲突

因为一个bmap代表的桶只存放8个键值对,所以需要有溢出桶保存溢出的键值对,溢出桶形成一个链,overflow则保存了下一个溢出桶的指针

map的所有桶都是连续的,其中mapextra.overflow 占据了一部分连续的空间用来分配溢出桶,每当hmap.buckets中的桶发生溢出时就会到mapextra.overflow 的空间中申请一个桶

runtime.mapassign 函数会在以下两种情况下触发扩容:

  1. 负载因子大于6.5

  1. 使用了太多溢出桶

golang对扩容使用的是增量扩容,其不是一次性完成,也不是原子操作

如果因溢出桶太多而扩容,那么会触发等量扩容(新桶数量不变),否则则触发普通扩容(新桶数量翻倍)。扩容时会创建新的桶和溢出桶,此时如果访问map,会在旧桶和新桶中查询值,如果写入或删除旧桶中的元素,则会导致目标旧桶中的元素被分发到对应的新桶。等量扩容则直接复制到新桶,普通扩容则会增加一位哈希掩码并根据新的掩码分发旧桶的元素到对应的两个新桶。旧桶:hash 11 -> 新桶:hash 011,111。当所有旧桶都被分流后,旧桶所对应的连续地址空间会被释放

注意:这里分流旧桶是指分流一个哈希掩码所对应的所有hmap和对应溢出桶的hmap链