2024年最全Golang 中的 map 详解_golang map(4),带你全面掌握高级知识点

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

noverflow  uint16         // 溢出桶的大概数量,当B小于16时是准确值,大于等于16时是大概的值
hash0      uint32         // 哈希种子,用于计算哈希值,为哈希函数的结果引入一定的随机性
buckets    unsafe.Pointer // 指向桶数组的指针,长度为 2^B ,如果元素个数为0,就为 nil
oldbuckets unsafe.Pointer // 指向一个旧桶数组,用于扩容,它的长度是当前桶数组的一半
nevacuate  uintptr        // 搬迁进度,小于此地址的桶数组迁移完成
extra      \*mapextra      // 可选字段,用于gc,指向所有的溢出桶,避免gc时扫描整个map,仅扫描所有溢出桶就足够了

}

// 溢出桶结构
type mapextra struct {
overflow *[]*bmap // 指针数组,指向所有溢出桶
oldoverflow *[]*bmap // 指针数组,发生扩容时,指向所有旧的溢出桶
nextOverflow *bmap // 指向所有溢出桶中下一个可以使用的溢出桶
}


bmap的结构:



type bmap struct {
tophash [bucketCnt]uint8 // bucketCnt=8,// 存放key哈希值的高8位,用于决定kv键值对放在桶内的哪个位置
}

//实际上编辑期间会动态生成一个新的结构体
type bmap struct {
topbits [8]uint8 // 存放key哈希值的高8位,用于决定kv键值对放在桶内的哪个位置
keys [8]keytype // 存放key的数组
values [8]valuetype // 存放value的数组
pad uintptr // 用于对齐内存
overflow uintptr // 指向下一个桶,即溢出桶,拉链法
}


  buckets是一个bmap数组,数组的长度就是 2^B。每个bucket固定包含8个key和value,实现上面是一个固定的大小连续内存块,分成四部分:tophash 值,8个key值,8个value值,指向下个bucket的指针。


  tophash 值用于快速查找key是否在该bucket中,当插入和查询运行时都会使用哈希哈数对key做哈希运算,获取一个hashcode,取高8位存放在bmap tophash字段中。


  桶里面会最多装 8 个 key,这些 key 之所以会落入同一个桶,是因为它们经过哈希计算后,哈希结果是“一类”的。在桶内,又会根据 key 计算出来的 hash 值的高 8 位来决定 key 到底落入桶内的哪个位置(一个桶内最多有8个位置)。


  如图,B=5 表示hmap的有2^5=32个bmap,buckets是一个bmap数组,其长度为32,每个bmap有8个key。


![在这里插入图片描述](https://img-blog.csdnimg.cn/6b7631e7ef7a4bacac4d9736717e531d.png)


  桶结构的很多字段得在编译时才会动态生成,比如key和values等


  桶结构中,之所以所有的key放一起,所有的value放一起,而不是key/value一对对的一起存放,目的便是在某些情况下可以省去pad字段,节省内存空间。由于内存对齐的原因,key0/value0/key1/value1… 这样的形式可能需要更多的补齐空间,比如 map[int64]int8 ,1字节的value后面需要补齐7个字节才能保证下一个key是 int64 对齐的。


  golang中的map使用的内存是不会收缩的,只会越用越多。


  每个 bucket 设计成最多只能放 8 个 key-value 对,如果有第 9 个 key-value 落入当前的 bucket,那就需要再构建一个溢出桶 bucket ,通过 overflow 指针连接起来。


### 四、map 的扩容


#### 1、装载因子(平均每个桶存储的元素个数)


  Go的装载因子阈值常量:6.5,map 最多可容纳 6.5\*2^B 个元素。


  装载因子等于 map中元素的个数 / map的容量,即len(map) / 2^B。装载因子用来表示空闲位置的情况,装载因子越大,表明空闲位置越少,冲突也越多。随着装载因子的增大,哈希表线性探测的平均用时就会增加,这会影响哈希表的性能,当装载因子大于70%,哈希表的性能就会急剧下降,当装载因子达到100%,整个哈希表就会完全失效,这个时候,查找和插入任意元素的复杂度都是O(N),因为需要遍历所有元素。


  另外装载因子与扩容、迁移等重新散列(rehash) 行为有直接关系:


* 在程序运行时,会不断地进行插入、删除等,会导致 bucket 不均,内存利用率低,需要迁移。
* 在程序运行时,出现装载因子过大,需要做扩容,解决 bucket 过大的问题。


**为什么装载因子是6.5?不是8?不是1?**  
   装载因子是哈希表中的一个重要指标,主要目的是为了平衡 buckets 的存储空间大小和查找元素时的性能高低。实际上这是 Go 官方的经过认真的测试得出的数字,一起来看看官方的这份测试报告。包含四个指标:  
 ![在这里插入图片描述](https://img-blog.csdnimg.cn/a752b1635d21424490c655c7ba7fa90f.png)


* loadFactor:负载因子,也叫装载因子;
* %overflow:溢出率,有溢出 bukcet 的百分比;
* bytes/entry:平均每对 key/alue 的开销字节数;
* hitprobe:查找一个存在的 key 时,要查找的平均个数;
* missprobe:查找一个不存在的 key 时,要查找的平均个数。


  Go 官方发现:装载因子越大,填入的元素越多,空间利用率就越高,但发生冲突的几率就变大;反之,装数因子越小,填入的元素越少,冲突发生的几率减小,但空间利用率低,而且还会提高扩容操作的次数。


  根据这份测试结果和讨论,Go 官方取了一个相对适中的值,把 Go 中的 map 的负数因子硬编码为 6.5,这就是 6.5 的选择缘由。这意味着在 Go 语言中,当 map存储的元素个数大于或等于 6.5\*桶个数 时,就会发扩容行为。


#### 2、触发 map 扩容的时机(插入、删除key)


* 当装载因子超过6.5时,扩容一倍,属于增量扩容;
* 当使用的溢出桶过多时,重新分配一样大的内存空间,属于等量扩容;  
 (实际上没有扩容,主要是为了回收空闲的溢出桶,节省空间,提高 map 的查找和插入效率)



> 
> 为什么会出现这种情况?  
>    这种情况可能是因为map删除的特性导致的。当我们不断向哈希表中插入数据,并且将他们又全部删除时,其内存占用并不会减少,因为删除只是将桶对应位置的tophash置nil而已。  
>    这种情况下,就会不断的积累溢出桶造成内存泄露,为了解决这种情况,采用了等量扩容的机制,一旦哈希表中出现了过多的溢出桶,会创建新桶保存数据,gc会清理掉老的溢出桶,从而避免内存泄露。
> 
> 
> 



> 
> 如何定义溢出桶是否太多需要等量扩容呢?两种情况:
> 
> 
> * 当B小于15时,溢出桶的数量超过2^B,属于溢出桶数量太多,需要等量扩容;
> * 当B大于等于15时,溢出桶数量超过2^15,属于溢出桶数量太多,需要等量扩容。
> 
> 
> 


#### 3、扩容策略(怎么扩容?)


  Go 会创建一个新的 buckets 数组,新的 buckets 数组的容量是旧buckets数组的两倍(或者和旧桶容量相同),将原始桶数组中的所有元素重新散列到新的桶数组中。这样做的目的是为了使每个桶中的元素数量尽可能平均分布,以提高查询效率。旧的buckets数组不会被直接删除,而是会把原来对旧数组的引用去掉,让GC来清除内存。


  在map进行扩容迁移的期间,不会触发第二次扩容。只有在前一个扩容迁移工作完成后,map才能进行下一次扩容操作。


#### 4、搬迁策略


  由于map扩容需要将原有的kv键值对搬迁到新的内存地址,如果一下子全部搬完,会非常的影响性能。go 中 map 的扩容采用渐进式的搬迁策略,原有的 key 并不会一次性搬迁完毕,一次性搬迁会造成比较大的延时,每次最多只会搬迁 2 个 bucket,将搬迁的O(N)开销均摊到O(1)的赋值和删除操作上。


  上面说的 hashGrow() 函数实际上并没有真正地“搬迁”,它只是分配好了新的 buckets,并将老的 buckets 挂到了 oldbuckets 字段上。真正搬迁 buckets 的动作在 growWork() 函数中,而调用 growWork() 函数的动作是在 mapassign 和 mapdelete 函数中。也就是插入或修改、删除 key 的时候,都会尝试进行搬迁 buckets 的工作。先检查 oldbuckets 是否搬迁完毕,具体来说就是检查 oldbuckets 是否为 nil。


### 五、解决哈希冲突


#### 1、开放寻址法


  如果发生哈希冲突,从发生冲突的那个单元起,按一定的次序,不断重复,从哈希表中寻找一个空闲的单元,将该键值对存储在该单元中。具体的实现方式包括线性探测法、平方探测法、随机探测法和双重哈希法等。开放寻址法需要的表长度要大于等于所需要存放的元素数量。


#### 2、链地址法


  基于数组 + 链表 实现哈希表,数组中每个元素都是一个链表,将每个桶都指向一个链表,当哈希冲突发生时,新的键值对会按顺序添加到该桶对应的链表的尾部。在查找特定键值对时,可以遍历该链表以查找与之匹配的键值对。


![img](https://img-blog.csdnimg.cn/img_convert/d97cfe28e00e7cea264db8964776775a.png)
![img](https://img-blog.csdnimg.cn/img_convert/2e40018c4b5b58982c12811515aa8a30.png)

**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

升。**

**[需要这份系统化的资料的朋友,可以添加戳这里获取](https://bbs.csdn.net/topics/618658159)**


**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**

  • 5
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值