go 数据结构map
要理解 map , 先要理解哈希表
哈希是一类算法(映射算法,有很多种),类似一元一次方程那种,比如取余。
而哈希表的结构一种实现方式是一种存储链表的数组。
首先他提供哈希函数创建数组(比如哈希算法是对7取余,则创建一个长度为7的存储链表的数组)。
当有数据需要存储的时候,先通过哈希函数确定应该存储到哪个数组下面。
但是数组的长度是有限的,而可能存储进来的数据是无限的, 那肯定就会出现不同的数据哈希结果相同的情况(哈希碰撞)。
为了解决这个问题,每个哈希表里数组的成员对应的都是一个链表,当出现重复的数据时, 就往链表中添加节点。
在查询的时候,先对需要查询的key进行取模,找到数组中对应的链表, 然后再遍历链表。
由上面的规则可以看出, 初始数组分配越大,则查询的时候越快,但是容易造成内存浪费,
相反, 如果数组分配的越小,则数据多的时候链表越长,查询越慢,但是被浪费的内存少。
所以在设计哈希表的时候,会考虑数组的扩容,流程大致如下:
1、先生成一个较小的存储链表的数组
2、等数据到达一定量的时候更换哈希算法,然后生成一个新的大点的存储链表的数组
3、将旧链表数组中的数据迁移到新的链表数组中(具体的迁移时间看情况,有些是在查询到某个key的时候才迁移这个key)
哈希表结构图大致如下 :
而我们的 map 可以理解为一种哈希表,使用拉链的方式消除哈希碰撞。
基于上面的认知, go 的map有两种重要的数据结构,hmap和bmap。
其中 hmap 充当了哈希表中数组的角色, bmap充当了链表的角色。
hmap中包含一个指向bmap数组的指针, key 经过哈希函数之后得到一个数, 这个数低位用于选择 bmap(当作bmap数组指针的下表), 高位用于放在bmap的[8]uint8数组中,用于快速试错。 然后一个bmap可以指向下一个bmap(拉链)。
hmap如上图。
go 的 map 中用于存储的结构是 bucket(即bmap)数组。而bucket结构如下:
他的核心部分是中间部分,我们使用的 map 中的 key和value都是存储在这里。
高位哈希值数组中记录的是当前 bucket 中 key相关的“索引”(有了这个就不用再遍历链表?)
另外一个字段就是指针。
go的map中也有应该哈希函数,哈希函数会把求得的值按照用途一分为二,高位和低位。
红色为低位,用于查找当前key属于 hmap 中的哪个bucket(链表),蓝色为高位, 用于定位bucket中的key。
【需要特别注意的一点是】,我们map中的key、value值都是存在同一个数组中的,数组的顺序是这样的:
并不是key0/value0/key1/value1的形式, 这样主要是为了消除内存对齐带来的空间浪费。
所以go里面的 map 整体结构应该如下 :
go里面的map同样存在扩容的需求, 他的实现方式如下:
当哈希表增长到一定程度的时候,go会将hmap中的bucket数组的数量扩充一倍,产生一个新的bucket数组,并将旧数组的数据迁移至新数组。
其中扩充的判断条件就是哈希表中的加载因子。
【需要注意的是】:在扩容迁移时,并不是立刻把旧的数组中的元素转移到新的bucket中,而是只有当访问到某个具体的bucket时,才会把bucket中的数据迁移到新的bucket中。
【注意】:这里并不会直接删除旧的bucket,而是把原来的引用去掉,利用GC清除内存。
删除map中的数据时,找到了map中的数据之后,针对key和value分别做如下操作:
1、如果``key``是一个指针类型的,则直接将其置为空,等待GC清除;
2、如果是值类型的,则清除相关内存。
3、同理,对``value``做相同的操作。
4、最后把key对应的高位值对应的数组index置为空。