【Go】 map 精髓理解

map

go map 的底层结构 hmap,的四个元素
然后再讲一下 buckets 的元素,讲一下 hash 冲突,和解决方法
再讲一下,增量扩容和等量扩容
再讲一下增删改查的过程,就查询过程

map 基础

向值为 nil 的 map 添加元素会发生 panic,说白了就是给声明但是没初始化的map添加元素
查询时候,如果没这个元素,会返回对应的零值,例如 int 的零值 0
一开始用 make 初始化时候,指定容量,可以有效的减少内存分配的次数,提升性能
查询语法if v, ok := m[“apple”];ok {}
删除语法:delete(m,“apple”) 空也不报错
添加和修改是一样的就是:m[“apple”] = “red”
len 也可以查长度
不是原子性的,并发不安全,多个协程同时操作一个 map 会产生读写冲突,panic,不过可以通过加锁或者 sync.map 实现并发安全

实现原理

1、数据结构

使用 Hash 表作为底层实现,一个 Hash 表可以有多个 bucket,每个 bucket 保存了一个或多个键值对,像,redis 是默认一个,go 是6.5个,多了会扩容
我们常说的 hash 桶就是 bucket,
【Go】 map 精髓理解_第1张图片

type Map struct {
	Key  *Type
	Elem *Type
	Bucket *Type
	Hmap   *Type
	Hiter  *Type
}

// hmap 简化版本
type hmap struct {
	count     int   // 元素个数,kv个数
	B         uint8 // bucket 数组大小,其实就是 bmap
	buckets    unsafe.Pointer // 指针指向 bucket 数组,数组长 2 的 B 次方
	oldbuckets unsafe.Pointer // 老旧 bucket 数组,用于扩容
  ......
}

type bmap struct {
  tophash [bucketCnt]uint8 // 存储 hash 值的高 8 位,就是长度为 8 的整型数组
  date []byte // kkkkkkvvvvvv数据,这样可以节省内存,避免了内存对齐
  overflow *bmap // 溢出 bucket 的地址,指向了下一个 bucket 将冲突的键连接起来
}

2、hash 冲突

当两个或以上数量的键被 hash 到同一个 bucket 时候,我们称这些键发生了冲突
Go 使用了链地址法解决冲突
因为每个 bucket 可以存放八个键值对,当同一个 bucket 存放超过 8 个键值对时候就会创建一个 bucket,用类似链表的方式将 bucket 连接起来,使用 *bucket 指向溢出的那部分

3、负载因子

负载因子 = 键数量 / bucket 数量
负载因子过大过小都不好
-过小,空间利用率较小
-过大,冲突严重,存取效率低

当负载因子过大,需要申请更多的 bucket 并且对所有的键值对重新排列,使其均匀的分配在 bucket,这个过程叫 rehash
每个 hash 表对负载因子的容忍度大小不一样,go 6.5 因为go的 bucket 可以存八个,redis 是1个

4、扩容

扩容有增量扩容,和等量扩容,都是对于负载因子过大的情况,对 go 就是当一个 bucket 有7/8个元素时候
增量扩容:
会新建一个 bucket ,新的 bucket 大小是原来两倍,然后把旧的 bucket 数组中的数据逐渐的搬迁到新的 bucket 数组
不过可能出现一个情况,当有数亿个元素时候,考虑到一次性搬迁会有很大延时,所有 go 用逐级搬迁策略,就是每次访问 map 都会搬迁一下,两个键值对
那它是如何处理数组指针呢?就是让 hmap 中 oldbuckets 指针指向原 buckets 数组,然后申请新的 buckets 数组,长度为原来两倍,之后的迁移工作就是将 oldbuckets 元素逐渐搬迁到新的 buckets ,搬迁完毕就可以释放 oldbuckets 数组
等量扩容:
这个不扩容,甚至内存释放了一些,意思是,不扩容的情况下,实行了类似增量扩容中搬迁的动作
因为在某些情况下,极端情况,我们删除了一些元素,很多哈,导致最后的元素集中于一个 bucket ,而这个 bucket 有很多的溢出 bucket 桶,删除后这些桶都空了,这时候等量扩容,就是使得 bucket 数量不变,但是重新组织后的 overflow 的 Bucket 数量减少,节省了空间,提高了查询效率

5、查询

查询就是:根据键的 Hash 值确定一个 bucket ,查它看有没有该元素
查询:
根据 key 值计算 Hash 值
取 Hash 值低位与 hmap.B 取模确定 bucket 位置
取 Hash 值高位,在 tophash 数组查询,如果tophash[i]的hash值与计算的一样,就去获取 tophash[i] 的key值
当前 bucket 没找到就去溢出bucket找
如果当前处于搬迁中,优先从 oldbucket 找
那么添加删除都差不多了,就不赘述了

参考自:《Go 专家编程》

你可能感兴趣的:(golang,开发语言,后端)