Go的map

192 阅读3分钟

Map的结构

//A header for a Go map.
type hmap struct {
	// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
	// Make sure this stays in sync with the compiler's definition.
	count     int // # live cells == size of map.  Must be first (used by len() builtin)
	flags     uint8
	B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
	noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
	hash0     uint32 // hash seed

	buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
	oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
	nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)

	extra *mapextra // optional fields

核心的字段: B:Buckets数组的长度就是2^B

buckets:指向了bucket数组,大小为2^B

buckets指向的结构体

type bmap struct {
    topbits  [8]uint8   //高八位,用来快速定位
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

bmap就是我们常说的桶,每个桶里面会装8个key,当超过8个key后,会新建bmap通过overflow链接。

我们看下bmap的图解

它的key和value是分来存储的,最后的overflow用来链接下一个bmap。HobHash是指某个key的hash值高八位(后面会讲到这有什么用)。

来一个整体的图

map的set过程

1).对key进行hash计算,得到的结果如图

10010111 | 000011110110110010001111001010100010010110010101010 │ 01010 2).计算落在哪个桶里 通过对hash值得后B位,如果B=5,那么通过上值得到10,因此我们放入第10个桶。 3).计算落在哪个桶里的哪个位置 如果第一个key是空,就放在这里,并且将高8位存入HobHash(存入后方便查询时迅速找到)。

hash冲突问题

在set时难免会遇到hash冲突,如果当前key已经有值,就放到下一个key。如果整个bucket满了,就放到overflow链接的下一个bucket。

map的get操作

10010111 | 000011110110110010001111001010100010010110010101010 │ 01010

1).寻找属于哪个桶 根据hash值的后B位找到位于哪个桶

2).寻找位于桶里具体位置 根据高8位直接定位到对应的HobHash,找到对应的key后进行比较,如果相等返回,不相等向下找寻,如果这个桶中找不到相应的key,则根据overflow去下一个桶中查找(正是因为key需要比较,所以 map的类型不可以是引用类型)

下图特别形象的展示了map的高位和低位如何定位桶数据

hash的扩容机制

map有两种扩容

当map中的元素越来越多,碰撞就会越来越多。增删改查效率会越来越低。 Go引入了装载因子:

loadFactor := count / (2^B)

count是元素的个数,(2^B)是桶的个数。

扩容机制: 1.装载因子超过6.5

也就是当map的元素个数大,而桶少,说明桶中元素过多,碰撞会非常激烈,因此需要进行扩容buckets,将B加1,相当于存放桶的list增长2倍。

2.当overflow的bucket过多,而实际上每个bucket装载的key又很少。我们需要申请新的bucket,将这些旧bucket存入这个新bucket,让key更加紧凑。

触发条件: 当B<15时,也就是 bucket 总数 2^B 小于 2^15 时:如果overflow的bucket数量超过2^B。

当B>=15时,也就是 bucket 总数 2^B 大于等于 2^15 时:如果overflow的bucket数量超过2^15。

扩容不是立即完成的,而是在每次修改和删除map的时候将旧的bucket转移到新的bucket中,每次转移两个。