Go语言map的底层实现

map是Go语言中基础的数据结构,在平常的使用中常常被用到。可是它底层是如何实现的呢?数组

map的总体结构图

Golang中map的底层实现是一个散列表,所以实现map的过程实际上就是实现散表的过程。在这个散列表中,主要出现的结构体有两个,一个叫hmap(a header for a go map),一个叫bucket。这两种结构的样子分别以下所示:bash

hmap:微信

图中有不少字段,可是便于理解map的架构,你只须要关心的只有一个,就是标红的字段:buckets数组。Golang的map中用于存储的结构是bucket数组。而bucket(即bmap)的结构是怎样的呢?数据结构

bucket:架构

相比于hmap,bucket的结构显得简单一些,标红的字段依然是“核心”,咱们使用的map中的key和value就存储在这里。“高位哈希值”数组记录的是当前bucket中key相关的“索引”,稍后会详细叙述。还有一个字段是一个指向扩容后的bucket的指针,使得bucket会造成一个链表结构。例以下图:函数

由此看出hmapbucket的关系是这样的:spa

这里写图片描述

而bucket又是一个链表,因此,总体的结构应该是这样的:3d

这里写图片描述

哈希表的特色是会有一个哈希函数,对你传来的key进行哈希运算,获得惟一的值,通常状况下都是一个数值。Golang的map中也有这么一个哈希函数,也会算出惟一的值,对于这个值的使用,Golang也是颇有意思。指针

Golang把求得的值按照用途一分为二:高位和低位。code

这里写图片描述

如图所示,蓝色为高位,红色为低位。 而后低位用于寻找当前key属于hmap中的哪一个bucket,而高位用于寻找bucket中的哪一个key。上文中提到:bucket中有个属性字段是“高位哈希值”数组,这里存的就是蓝色的高位值,用来声明当前bucket中有哪些“key”,便于搜索查找。 须要特别指出的一点是:咱们map中的key/value值都是存到同一个数组中的。数组中的顺序是这样的:

这里写图片描述

并非key0/value0/key1/value1的形式,这样作的好处是:在key和value的长度不一样的时候,能够消除padding带来的空间浪费。

如今,咱们能够获得Go语言map的整个的结构图了:

这里写图片描述

以上,就是Go语言map的总体结构了。

map的扩容

当以上的哈希表增加的时候,Go语言会将bucket数组的数量扩充一倍,产生一个新的bucket数组,并将旧数组的数据迁移至新数组。

加载因子

判断扩充的条件,就是哈希表中的加载因子(即loadFactor)。

加载因子是一个阈值,通常表示为:散列包含的元素数 除以 位置总数。是一种“产生冲突机会”和“空间使用”的平衡与折中:加载因子越小,说明空间空置率高,空间使用率小,可是加载因子越大,说明空间利用率上去了,可是“产生冲突机会”高了。

每种哈希表的都会有一个加载因子,数值超过加载因子就会为哈希表扩容。 Golang的map加载因子的公式是:map长度 / 2^B阈值是6.5。其中B能够理解为已扩容的次数。

当Go的map长度增加到大于加载因子所需的map长度时,Go语言就会将产生一个新的bucket数组,而后把旧的bucket数组移到一个属性字段oldbucket中。注意:并非马上把旧的数组中的元素转义到新的bucket当中,而是,只有当访问到具体的某个bucket的时候,会把bucket中的数据转移到新的bucket中。

以下图所示:当扩容的时候,Go的map结构体中,会保存旧的数据,和新生成的数组

这里写图片描述

上面部分表明旧的有数据的bucket,下面部分表明新生成的新的bucket。蓝色表明存有数据的bucket,橘黄色表明空的bucket。 扩容时map并不会当即把新数据作迁移,而是当访问原来旧bucket的数据的时候,才把旧数据作迁移,以下图:

这里写图片描述

注意:这里并不会直接删除旧的bucket,而是把原来的引用去掉,利用GC清除内存。

map中数据的删除

若是理解了map的总体结构,那么查找、更新、删除的基本步骤应该都很清楚了。这里再也不赘述。 值得注意的是,找到了map中的数据以后,针对key和value分别作以下操做:

一、若是``key``是一个指针类型的,则直接将其置为空,等待GC清除;
二、若是是值类型的,则清除相关内存。
三、同理,对``value``作相同的操做。
四、最后把key对应的高位值对应的数组index置为空。
复制代码

更多精彩内容,请关注个人微信公众号 互联网技术窝 或者加微信共同探讨交流:

相关文章
相关标签/搜索