Go——map操作及原理

一.map介绍和使用

        map是一种无序的基于key-value的数据结构,Go语言的map是引用类型,必须初始化才可以使用。

        1. 定义

        Go语言中,map类型语法如下:

map[KeyType]ValueType
  • KeyType表示键类型
  • ValueType表示值类型

        map类型的变量默认初始值为nil,需要使用make函数来分配内存。语法为:

make(map[KeyType]ValueType, [cap])map[KeyType]ValueType{} //底层也是使用的makemap[KeyType]Value{ //底层也是使用的makekey:value,key:value,...
}

        其中cap表示map的容量,该参数虽然不是必须的,但是我们应该在初始化map的时候就为其指定一个合适的容量。 

        可以使用len()内置函数来获取map键值对的个数。

        注意:map保存的键值对中,键不能被修改,只能修改值。

        2.基本使用

package mainimport "fmt"func main() {scoreMap := make(map[string]int, 8)scoreMap["张三"] = 100scoreMap["小明"] = 90fmt.Println(scoreMap)fmt.Printf("key num is %d\n", len(scoreMap))fmt.Println(scoreMap["小明"])fmt.Printf("type(scoreMap)=%T\n", scoreMap)
}

map也支持在声明时填充元素: 

package mainimport "fmt"func main() {userInfo := map[string]string{"username": "zhansan","password": "123456",}fmt.Println(userInfo)
}

        3. 判断某个键是否存在

        Go语言中有个判断map中的键是否存在的特殊写法:

value, ok := map[key]

        例子:

package mainimport "fmt"func main() {userInfo := map[string]string{"username": "zhansan","passward": "123456",}value, ok := userInfo["passward"]if ok {fmt.Println(value)} else {fmt.Println("passward is not exit")}value, ok = userInfo["sex"]if ok {fmt.Println(value)} else {fmt.Println("sex is not exit")}
}

        4. map的遍历

        遍历key和value:

package mainimport "fmt"func main() {scoreMap := make(map[string]int, 8)scoreMap["小明"] = 100scoreMap["张三"] = 80scoreMap["李四"] = 60for key, value := range scoreMap {fmt.Printf("scoreMap[%s] = %d\n", key, value)}
}

         只遍历key:

注意:遍历map时的元素顺序与添加键值对的顺序无关。 

        5. 删除键值对

        使用delete()内置函数从map中删除一组键值对,格式如下:

delete(map, key)//map:为需要删除键值对的map
//key:表示要删除键值对的键
package mainimport "fmt"func main() {scoreMap := make(map[string]int, 8)scoreMap["小明"] = 100scoreMap["张三"] = 80scoreMap["李四"] = 60value, ok := scoreMap["李四"]if ok {fmt.Println(value)} else {fmt.Println("李四 is not exit")}//删除键值对delete(scoreMap, "李四")value, ok = scoreMap["李四"]if ok {fmt.Println(value)} else {fmt.Println("李四 is not exit")}
}

        6. 按照指定顺序遍历map

        实际时先获取到所有的键,将键设置成指定顺序,再通过键来遍历map。

package mainimport ("fmt""math/rand""sort""time"
)func main() {rand.Seed(time.Now().UnixNano()) //初始化随机种子scoreMap := make(map[string]int, 200)for i := 0; i < 100; i++ {key := fmt.Sprintf("stu%02d", i)scoreMap[key] = rand.Intn(100) //获取0-100的随机数//fmt.Println(key, scoreMap[key])}keys := make([]string, 0, 200)//保存key//按照排序后的key遍历scoreMapfor key := range scoreMap {keys = append(keys, key)}//fmt.Println(keys)sort.Strings(keys) //对keys进行排序for _, key := range keys {fmt.Printf("scoreMap[%s] = %d\n", key, scoreMap[key])}
}

        7. 元素为map类型的切片

package mainimport "fmt"func main() {mapSlice := make([]map[string]string, 3, 10) //并没有为map分配地址空间for index, val := range mapSlice {fmt.Printf("mapSlice[%d] = %v\n", index, val)}//分配地址空间for index, _ := range mapSlice {mapSlice[index] = make(map[string]string, 10)}fmt.Println("---------插入键值对后---------")//插入键值对mapSlice[0]["name"] = "张三"mapSlice[0]["passwd"] = "123123"mapSlice[1]["name"] = "李四"mapSlice[1]["passwd"] = "321321"mm := map[string]string{"name":   "小明","passwd": "123465",}mapSlice = append(mapSlice, mm)for index, val := range mapSlice {fmt.Printf("mapSlice[%d] = %v\n", index, val)}
}

        8. 值为切片类型的map

package mainimport "fmt"func main() {sliceMap := make(map[string][]string, 10) //没有为slice分配空间sliceMap["中国"] = make([]string, 0, 10)sliceMap["中国"] = append(sliceMap["中国"], "北京", "上海", "长沙")key := "美国"value, ok := sliceMap[key]if !ok {value = make([]string, 0)}value = append(value, "芝加哥", "华盛顿")sliceMap[key] = valuefor key, val := range sliceMap {fmt.Printf("sliceMap[%s] = %v\n", key, val)}
}

二.map底层原理

         Go语言的map底层数据结构为哈希表(散列表),但是与C++的哈希表实现不同。想要了解Go语言map的底层实现,需要先了解两个重要的数据结构 hmap和bmap。

        2.1 map头部数据结构——hmap

        hmap中有几个重要的属性:

  • count:记录了map中实际元素的个数
  • B:控制哈希桶的个数为2^B个
  • buckets:是一个指向长度为2^B大小的类型为bmap的数组
  • oldbuckets:与buckets一样也是指向一个多桶的数组,不同的是oldbuckets指向的是旧桶的地址,当oldbuckets不为空时,表示map正处于扩容阶段。
type hmap struct {// map中元素的个数,使用len返回就是该值count     int// 状态标记// 1: 迭代器正在操作buckets// 2: 迭代器正在操作oldbuckets // 4: go协程正在像map中写操作// 8: 当前的map正在增长,并且增长的大小和原来一样flags     uint8// buckets桶的个数为2^BB         uint8 // 溢出桶的个数noverflow uint16 // key计算hash时的hash种子hash0     uint32// 指向的是桶的地址buckets    unsafe.Pointer// 旧桶的地址,当map处于扩容时旧桶才不为niloldbuckets unsafe.Pointer //扩容之后数据迁移的计数器,记录下次迁移的位置,当nevacuate>旧桶元素个数,数据迁移完nevacuate  uintptr // 额外的map字段,存储溢出桶信息// 这个字段是为了优化GC扫描而设计的。当key和value均不包含指针,并且都可以inline时使用。extra是指向mapextra类型的指针。extra *mapextra
}

        创建一个map实际是创建一个指针,指向hmap结构。

        mapextra结构: 

        如果一个哈希表要分配桶的数目大于2^4个,就认为使用溢出桶的几率比较大,就会预分配2^(B-4)个溢出桶备用,这些溢出桶与常规桶内存中是连续的,只是前2^B个作为常规桶。

type mapextra struct {// 如果 key 和 value 都不包含指针,并且可以被 inline(<=128 字节)// 就使用 hmap的extra字段 来存储 overflow buckets,这样可以避免 GC 扫描整个 map// 然而 bmap.overflow 也是个指针。这时候我们只能把这些 overflow 的指针// 都放在 hmap.extra.overflow 和 hmap.extra.oldoverflow 中了// overflow 包含的是 hmap.buckets 的 overflow 的 buckets// oldoverflow 包含扩容时的 hmap.oldbuckets 的 overflow 的 bucketoverflow    *[]*bmap //记录已使用的溢出桶的地址oldoverflow *[]*bmap //扩容阶段旧桶使用的溢出桶地址// 指向空闲的 overflow bucket 的指针nextOverflow *bmap //指向下一个空闲溢出桶
}

        2.2 bmap

        bmap是每一个桶的数据结构,每一个bmap包含8个key和value。

type bmap struct {tophash [bucketCnt]uint8        // len为8的数组,用来快速定位key是否在这个bmap中// 一个桶最多8个槽位,如果key所在的tophash值在tophash中,则代表该key在这个桶中
}

         上面是bmap的静态结构,在编译过程中runtime.bmap会扩展成以下结构:

  • topbits :用来快速定位桶中键值对的位置。
  • keys:键值对的键
  • values:键值对的值
  • overflow:当8个key满的时候,需要新创建一个桶,overflow保存下一个桶的地址。

细节:

        这里将键和键保存到了一起,值和值保存在了一起,为什么不讲键和值保存在一起?

        因为键和值的类型可能不同,结构体内存对齐会浪费空间。

type bmap struct{topbits  [8]uint8keys     [8]keytypevalues   [8]valuetypepad      uintptr        // 内存对齐使用,可能不需要overflow uintptr        // 当bucket 的8个key 存满了之后// overflow 指向下一个溢出桶 bmap,// overflow是uintptr而不是*bmap类型,保证bmap完全不含指针,是为了减少gc,溢出桶存储到extra字段中
}

        2.3 整体结构示意图

  • 如下图,创建一个容量为5的map,此时B=5,分配桶数为2^5=32个(为[]bmap下标0-31),则备用溢出桶数为2^(5-4)=2个(为[]bmap下标32,33)。
  • 此时,0号的bmap桶满了,overflow指向下一个溢出桶地址,即[]bmap下标为32位置。
  • hmap中的noverflow表示使用溢出桶数量,这里为1,extra字段记录溢出桶mapextra结构体。
  • mapextra中的overflow保存使用的溢出桶,nextoverflow指向下一个空闲溢出桶33号。

        创建一个map,Go语言底层实际调用的是makemap函数,主要做的工作就是初始化hmap结构体的各个字段。比如:计算B的大小,设置哈希种子hash0,给buckets分配内存等。

func makemap(t *maptype, hint int, h *hmap) *hmap {//计算内存空间和判断是否内存溢出mem, overflow := math.MulUintptr(uintptr(hint), t.bucket.size)if overflow || mem > maxAlloc {hint = 0}// initialize Hmapif h == nil {h = new(hmap)}h.hash0 = fastrand()//计算出指数B,那么桶的数量表示2^BB := uint8(0)for overLoadFactor(hint, B) {B++}h.B = Bif h.B != 0 {var nextOverflow *bmap//根据B去创建对应的桶和溢出桶h.buckets, nextOverflow = makeBucketArray(t, h.B, nil)if nextOverflow != nil {h.extra = new(mapextra)h.extra.nextOverflow = nextOverflow}}return h
}

         2.4 key定位原理

        key通过哈希计算后得到哈希值,哈希值的低B位用于确定桶,哈希值的高8位,用于在一个独立的桶中找到键的位置。

        例子:

        当在6号buckets中每有找到对应的tophash,并且overflow不为空,还需要继续到overflow指向的buckets中的tophash中查找,直到找到或者所有的key槽位都找遍,包括该buckets下的所有溢出桶(overflow)。

        2.5 插入元素

  • 插入

        key通过哈希函数得到哈希值,通过低B位确定桶位置,在桶中按顺序找空位置,找到后,将高8位保存在tophash中,key和value保存到keys和values中。如果当前桶中没有空闲位置,查看是否有溢出桶,有的话,在溢出桶中找空位置保存。没有溢出桶,添加溢出桶,将数据保存到第一个空位置。

  • 哈希冲突

         当两个不同的key键通过哈希函数落到了同一个桶中,这时就发生了哈希冲突。

        Go语言的解决办法是链地址法:由于桶(bmap)的数据结构,一个桶保存8个键和值。在桶中按顺序寻找第一个空位,若有空位,则将其置于其中;若没有空位,判断是否有溢出桶;若有溢出桶,在溢出桶中寻找空位。若没有溢出桶,则添加溢出桶,并将其置于溢出桶的第一个空位(非扩容的情况)。

        当不同的key通过哈希函数得到的哈希值相同时,低位和高位都相同,如何查找到对应的键值对?

        步骤和上面相同,低B为找到对应的桶,高8位找到对应的tophash位置,拿出key与要找的key比较是否相同,相同的话取出,不同的话,再在tophash中查找哈希值高8位的值,没找到,在溢出桶中找,直到找完所有key或者找到对应的key为止。

        2.6 扩容

        为什么要进行扩容:

        当元素越来越多或者溢出桶的数量越来越多,导致查找的效率会变低。

                2.6.1 负载因子

        负载因子是决定哈希表是否进行扩容的关键指标。负载因子的值为6.5(经验所得),意思是平均每个桶中键值对的数量。当 总键值对个数 >= 桶总数 * 6.5,这个时候说明大部分的桶可能快满了。这个时候就可能需要扩容。

                2.6.2 扩容的条件

扩容的条件有两个:

  1. 判断负载因子是否达到临界点(6.5),如果达到了,如果插入新的元素,大概率会需要挂在溢出桶上了。
  2. 判断溢出桶是否过多,当正常桶总数< 2^15时,如果溢出桶总数>=正常桶总数,则认为溢出桶过多。当正常桶总数>=2^15时,直接与2^15比较,当溢出桶总数>=2^15时,则认为溢出桶总数过多。

        其实第二点是对第一点的补充。因为在负载因子比较小的情况下,有可能map的查找和插入的效率也可能很低。即map里的元素少,但是桶数量多(真实分配的桶数量多,包括大量的溢出桶)。

        导致上面这种情况的原因是:对map中的元素不断的增删,增加会导致桶的数量变多,删除导致负载因子不高。

        这样导致桶的使用率不高,存储的值比较稀疏,导致查找的效率变低。

        2.6.3 解决方案

  • 针对超过负载因子的情况:将B+1,新建一个buckets数组,新的buckets大小是原来的2倍,然后将旧buckets数据搬迁到新的buckets。该方法我们称之为增量扩容。

  • 针对桶数量过多的情况:并不扩大容量,buckets数量维持不变,重新做一遍类似增量扩容的操作,就是将键值对重新映射到新buckets中,使得buckets的使用率变高。该方法我们称为等量扩容。

        对于等量扩容,其实存在一种极端情况:如果插入map的key通过哈希函数后得到的哈希值一样,那它们就会落到同一个buckets中,查过8个就会产生overflow,结果也会照成溢出桶过多,移动元素其实解决不了问题,此时哈希表退化成了一个链表,操作效率编程了O(n),但Go的每一个map都会在初始阶段的makemap是定义一个随机哈希种子,所以要构成这种冲突是没有那么容易的。

        扩容:首先分配新的buckets(不管增量扩容还是等量扩容都要新分配buckets),将老的buckets挂到oldbuckets字段上。buckes挂上新的buckets。然后将oldbuckets上的键值对重新哈希到buckets上,直到旧桶中的键值对全部搬迁完毕后,删除oldbuckets。 当oldbuckets值为nil表示扩容完毕。

        2.6.4 渐进式扩容

        由于map扩容需要使用将原有的键值对重新搬迁到新的内存地址,如果map储存了数以亿计的键值对,一次性搬迁会照成比较大的延时。因此Go语言map扩容采取了一种渐进式的方式。

        原有的key不会一次性搬迁完毕,每次最多搬迁2个buckets,并且只有在插入,修改或删除key的时候,才会进行搬迁buckets工作。

参考文档:深入解析Golang的map设计 - 知乎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/776329.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Altair-一个被名字耽误的超强交互式可视化库

今天的推文我们介绍一个功能很强,但知名度不如Matplotlib、pyecharts等静态或者交互式可视化库-Altair。Altair是基于Vega和Vega-Lite的Python数据统计可视化库&#xff0c;其优秀的交互、数据统计功能和清新的配色&#xff0c;很难让人用过就忘记(唯一不好就是名字太难记啦!)。…

2024年的黑马项目,在视频号上开小店,这个机会不容错过!

大家好&#xff0c;我是电商小布。 在互联网的快速发展下&#xff0c;电商这一行可以说是展现出来了前所未有的生机。 也造就了越来越多项目的产生&#xff0c;视频号小店就是其中之一。 有人说&#xff0c;就今年的各大项目情况来看&#xff0c;视频号小店无疑是最大的黑马…

jsp将一个文本输入框改成下拉单选框,选项为字典表配置,通过后端查询

一&#xff0c;业务场景&#xff1a; 一个人员信息管理页面&#xff0c;原来有个最高学历是文本输入框&#xff0c;可以随意填写&#xff0c;现在业务想改成下拉单选框进行规范化&#xff0c;在专科及以下、本科、研究生三个选项中选择&#xff1b; 二&#xff0c;需要解决问…

【Linux】 gcc(linux下的编译器)程序的编译和链接详解

目录 前言&#xff1a;快速认识gcc 1. 程序的翻译环境和执行环境 2.编译和链接 2.1翻译环境 2.2编译环境 1. 预处理 gcc -E指令 test.c&#xff08;源文件&#xff09; -o test.i&#xff08;生成在一个文件中&#xff0c;可以自己指定&#xff09; 预处理完成之后就停下来&am…

LeetCode 1027——最长等差数列

阅读目录 1. 题目2. 解题思路3. 代码实现 1. 题目 2. 解题思路 假设我们以 f[d][nums[i]]表示以 nums[i] 为结尾元素间距为 d 的等差数列的最大长度&#xff0c;那么&#xff0c;如果 nums[i]-d 也存在于 nums 数组中&#xff0c;则有&#xff1a; f [ d ] [ n u m s [ i ] ] …

解决vim中NERDTree图标是问号或者乱码问题

解决vim中NERDTree图标是问号或者乱码问题 乱码信息如图解决办法1. 安装字体下载字体安装字体Ubuntu系统Windows11系统 2. 控制台修改字体Ubuntu系统Windows11系统 乱码信息如图 Ubuntu系统上的情况 使用windows控制台连接的情况 解决办法 1. 安装字体 下载字体 在nerd f…

51单片机学习9 串口通讯

51单片机学习9 串口通讯 一、串口通讯简介UARTSTC89C51RC/RD的串口资源 二、51单片机串口介绍1. 内部结构2. 寄存器&#xff08;1&#xff09;串口控制寄存器SCON&#xff08;2&#xff09;电源控制寄存器PCON 3. 计算波特率4. 串口配置步骤 三、 开发示例1. 硬件电路2. 代码实…

网络面试——浏览器输入url到显示主页的过程

浏览器输入URL到显示主页的过程通常可以分为以下步骤&#xff1a; 1. **URL解析**&#xff1a; - 当用户在浏览器的地址栏中输入URL时&#xff0c;浏览器会首先对该URL进行解析。 - 解析URL包括识别协议&#xff08;例如HTTP、HTTPS&#xff09;、主机名&#xff08;例如…

YOLOv5-小知识记录(一)

0. 写在前面 这篇博文主要是为了记录一下yolov5中的小的记忆点&#xff0c;方便自己查看和理解。 1. 完整过程 &#xff08;1&#xff09;Input阶段&#xff0c;图片需要经过数据增强Mosaic&#xff0c;并且初始化一组anchor预设&#xff1b; &#xff08;2&#xff09;特征提…

MSA7T10 DVBT2高清机顶盒方案

一、方案描述 MSA7T10系列芯片是Mstar推出的极富竞争力的DVB-T2机顶盒FTA方案&#xff0c;芯片内置64MB DDR2和T2解调器&#xff0c;支持T2 1.3.1规范&#xff0c;支持HEVC&#xff0c;H.264&#xff0c;MPEG高清视频&#xff0c;支持PVR/Timeshit功能&#xff0c;支持各种多媒…

曲线生成 | 图解Reeds-Shepp曲线生成原理(附ROS C++/Python/Matlab仿真)

目录 0 专栏介绍1 什么是Reeds-Shepp曲线&#xff1f;2 Reeds-Shepp曲线的运动模式3 Reeds-Shepp曲线算法原理3.1 坐标变换3.2 时间翻转(time-flip)3.3 反射变换(reflect)3.4 后向变换(backwards) 4 仿真实现4.1 ROS C实现4.2 Python实现4.3 Matlab实现 0 专栏介绍 &#x1f5…

如何利用社媒群组如何高效开发国外客户

现在社媒营销也是越来越流行了&#xff0c;很多外贸人都开始做社媒营销。社媒营销相对来说是比较有温度的一个营销&#xff0c;因为大部分社媒平台都支持在线聊天&#xff0c;触达的即时性是比较高的&#xff0c;效果也比传统的一些方法要好一些。 当然做社媒也是有难度的&…

西藏实景三维技术研讨交流会成功举办

2024年3月21-22日&#xff0c;西藏自治区“实景三维技术研讨交流会”在拉萨成功举办。 本次会议由西藏自治区自然资源厅、自然资源部重庆测绘院指导&#xff0c;西藏自治区测绘学会、西藏自治区地理信息产业协会主办&#xff0c;武汉大势智慧科技有限公司&#xff08;后简称“…

数据库-索引快速学

索引 当表中数据量庞大时&#xff0c;往往搜索一条数据就会耗费很长的时间等待 索引是帮助数据库高效获取数据的数据结构 create index 索引名 on 数据表名&#xff08;字段名&#xff09;;为该表下的某一字段创建索引&#xff0c;检索耗时会大大的减小 索引的优缺点 优点&…

【Python BUG】CondaHTTPError解决记录

问题描述 CondaHTTPError: HTTP 429 TOO MANY REQUESTS for url https://mirrors.ustc.edu.cn/anaconda/pkgs/free/win-64/current_repodata.json Elapsed: 00:26.513315 解决方案 找到用户路径下的 .condarc文件&#xff0c;建议用这个方法前和我一样做个备份&#xff0c;方…

python中类的导入与使用

1、类的介绍 与C中面向对象思想类似&#xff0c;有时候为了方便&#xff0c;需要专门创建一个类&#xff0c;将相关的函数全部写入到该类中&#xff0c;方便后续创建对象&#xff0c;再使用类中函数。那么如何创建完类&#xff0c;在其他文件中使用类中函数&#xff0c;这是这篇…

Python Flask框架 -- flask-migrate迁移ORM模型

# 之前使用的这个db.create_all()很有局限性&#xff0c;它不能把在class里修改的东西同步上数据库&#xff0c;所以不用了 # with app.app_context(): # 请求应用上下文 # db.create_all() # 把所有的表同步到数据库中去 例如&#xff0c;在User类中增加一个email字段&…

STM32和GD32内部时钟与外部时钟讲解

STM32F103为例: 1. 当 HSI 被用作 PLL 时钟输入时,可以实现的最大系统时钟频率为 64 MHz。 2. 要使 USB 功能可用,必须同时启用 HSE 和 PLL,并使 USBCLK 运行在 48 MHz。 3. 要实现 ADC 转换时间为 1 s,APB2 必须为 14 MHz、28 MHz 或 56 MHz。 ①. HSE = 高速外部时钟信号…

[linux初阶][vim-gcc-gdb] OneCharter: vim编辑器

一.vim编辑器基础 目录 一.vim编辑器基础 ①.vim的语法 ②vim的三种模式 ③三种模式的基本切换 ④各个模式下的一些操作 二.配置vim环境 ①手动配置(不推荐) ②自动配置(推荐) vim是vi的升级版,包含了更加丰富的功能. ①.vim的语法 vim [文件名] ②vim的三种模式 命令…

爬取搜狗翻译项目实例

视频中讲解的是百度翻译&#xff0c;但是视频中的方法现在已经不适用了&#xff0c;因为他们对 URL 的参数进行了修改&#xff0c;导致没法直接修改参数来爬取对应的翻译结果&#xff0c;这里我使用搜狗翻译来做演示&#xff0c;原理是一样的。 我们搜索的关键字会返回在 URL 中…