map的底层是采用哈希表来实现的。

数据结构

底层使用一个数组,数组中的每个位置叫做桶,数组的长度叫做桶的数量。每个键值对要放到一个桶当中。 其数据结构为runtime/map.go/hamp

type hmap struct {
	count int // 当前键值对的个数
	B uint8 //bucket数组的大小
	buckets unsafe.Pointer //bucket数组指针,数组的大小为2^B
	....
}

buckets指向的桶数组中,每个桶的定义如下:

type bmap struct {
	tophash [8]uint8 //哈希值的高8位
	data byte[1]
	overflow *bmap //溢出的bucket地址
}

tophash存放哈希值的高8位,可以存储8个,所以一个bmap可以存储8个键值对,当超过8个之后,就需要通过overflow以形如链表的方式组织起来。

从图中可以看到,bucket中键值对是以先存储键,再存储值的方式进行存储的,这样可以节省字节对齐带来的空间浪费。

扩容

使用负载因为来衡量哈希表的冲突情况

负载因子 = 键数量 / bucket数量

Go在负载因子达到6.5的时候才会触发扩容,因为GO的每个bucket可以存8个键值度,所以容忍的负载因子要更高一些。

扩容的过程与redis的类似,都是渐进式扩容。不过这也分两步,增量扩容和等量扩容。

增量扩容

负载因子过大的时候,就新建一个bucket,新的bucket的长度为原来的二倍。将旧的键值对搬移到新的buckets中,不过这个搬移的过程不是一下子就完成的,而是在每次访问map的时候触发一次搬移,每次搬移两个键值对,直到旧的bucket中的键值对搬移结束,扩容结束。

扩容的时候会让buckets指向新申请的更大的bucket,而让oldbuckets指向旧的buckets,搬移结束后oldbuckets删除。

等量扩容

等量扩容是因为可能存在键值对过于集中在某个桶中(大量增删操作后,剩余的键集中在一个桶上),overflow的bucket数量增多,但负载因子又不高,从而无法执行增量搬移的极端情况。 这种情况下访问的效率会非常差,所以需要进行一次等量扩容,将键值对分散开来,既节省空间又提高了访问效率。

查找过程

  1. 根据key值算法哈希值
  2. 取哈希值低位与hmap.B取模确定bucket的位置
  3. 取哈希值高位在tophash数组中查询
  4. 如果tophash[i]中存储值和哈希值相同,则去找到改bucket中的key值进行比较
  5. 如果没有找到,则去下一个overfilw的bucket中查找。
  6. 如果当前处于扩容状态,则优先从oldbuckes中查找。 没有找到结果,也不会返回空值,而是返回相应类型的零值。

插入过程

  1. 根据key值算出哈希值
  2. 取哈希值低位与hmap.B取模确定bucket位置
  3. 查找该key是否已经存在,如果存在则直接更新值
  4. 如果没找到key,将key 插入。