Go语言入门之Map详解

news2024/11/15 19:47:35

Go语言入门之Map详解

1.基础定义

map是一个无序的,k-v键值对格式的集合

(1)特点

  • 类型特点:map为引用类型,所以在函数中更新value值会永久改变
  • 顺序特点:map的遍历是无序的,因为底层是哈希表,哈希表无序
  • 初始化使用:0值或者未初始化值为nil,未初始化不可赋值使用,否则直接panic
  • 键值对:key和value总是成对出现,key是唯一的,可以是任何可比较类型
  • 动态性:可以在运行时动态地增加或删除键值对,而不需要预先声明大小
  • 快速查找:map提供了快速查找、插入和删除操作,平均时间复杂度O(1)
  • 并发:非线程安全的,保证安全需要加锁

(2)定义声明

var name map[key_type]value_type
  • name:变量名
  • key_type:键的类型
  • value_type:值的类型
// 方式一
var m map[int]string = map[int]string{}

// 方式二
m := map[int]string{
    1 : "老一",
    2 : "老二",
    3 : "老三",
}

// 方式三:5代表容量,也就是在内存中占用多大的空间,可以省略
m := make(map[int]string,5)

2.基本使用

(1)添加元素

  • 1.最常见的就是通过字面量声明map的时候进行添加,如上方式2
  • 2.其次是直接给指定键设置对应的值
mapName[key] = value

// 假设map名为m,key为int,value为string
m[5] = "老五"

(2)删除元素

根据键删除元素,删除不存在的key也不会报错

delete(mapName, key)  

// 假设map名为m,key为int,value为string
delete(m, 5)

(3)修改元素

修改直接修改指定键对应的值就可以

mapName[key] = newValue 

// 假设map名为m,key为int,value为string
m[5] = "五"

(4)获取元素

根据键获取值,ok 为是否找到的标志位,类型为布尔

如果未找到值,不会报错,会返回对应类型的空值

value, ok := mapName[key]
if !ok {
		fmt.Println(ok)
	}

(5)遍历所有元素

注意:map的遍历是无序的

for key, value := range myMap {
   // 处理每对键值
}

// 例子
for i, s := range m {
		fmt.Println(i, s)
	}

3.底层原理

golang语言的map底层本质是用hashmap进行实现的,所以map本质上是哈希表

哈希表是一种使用哈希函数组织数据,以支持快速插入和搜索的数据结构。

哈希函数,又名散列函数,是一种将任意长度的输入(如字符串)通过特定的散列算法,变换成固定长度的输出的函数。通常会使用类似数组的形式来存储哈希值,从而保证哈希值的访问性能。

如果输入的范围超出映射输出的范围,就可能会导致不同的输入得到相同的输出,这就是哈希冲突

解决这种问题通常两种方式:开放地址法拉链法

(1)map的实现方案

开放地址法:

通常使用数组实现数据结构

  • 1.首先数组是由不同的哈希值组成,称为哈希表
  • 2.然后进行很多键,进行哈希函数确认地址放入相应位置,如果哈希表的这个槽位已经被占用,使用探测序列(如线性探测、二次探测或双重散列等)来找到下一个可用的槽位,并将冲突的键存储在那里。

缺点:

这种方法要求更多的空间来解决冲突,因为不仅要存储数据,还需要额外的空间来解决碰撞。

拉链法(go语言的map使用了该方法):

通常使用数组和链表作为底层数据结构

  • 1.首先数组是由不同的哈希值组成,称为哈希表,哈希表每个槽位都将存储一个链表
  • 2.然后会进来许多键,不同键进行hash后,求模算出hash值,链接到数组上,哈希值相同的情况(哈希碰撞),新进来的键就会挂在已有键链表的后面
  • 3.当需要查找特定键时,首先使用哈希函数确定其位置,然后在该位置的链表上进行线性搜索,直到找到匹配的键或者达到链表的末尾。

数组不同索引处链接的链表也被称之为桶(Bucket)

(2)map的底层结构

hmap
type hmap struct {
 count     int // 当前哈希表中的元素数量,即键值对数量,可用内置函数len()获取
 flags     uint8  // 标志位,标记map状态和属性的字段,如正在迭代等状态
 B         uint8  // 表示哈希表桶(buckets)的数量为2的B次方
 noverflow uint16 // 溢出桶的大致数量,扩容时会用到
 hash0     uint32 // 哈希种子,对key做哈希是加入种子计算哈希值,确保map安全性

 buckets    unsafe.Pointer // 存储桶数组的指针
 oldbuckets unsafe.Pointer // 扩容时用于保存旧桶数组的指针 , 大小为新桶数组的一半
 nevacuate  uintptr        // 扩容时的迁移进度器,迁移桶下标小于此值说明完成迁移

 extra *mapextra // 溢出桶的指针,指向mapextra结构体,用于存储一些额外的字段和信息
}
// mapextra 处理桶溢出的结构体
type mapextra struct {
 overflow    *[]*bmap    // 溢出桶数组指针,仅当key和elem非指针时才使用
 oldoverflow *[]*bmap    // 旧的溢出桶数组指针,仅当key和elem非指针时才使用

 nextOverflow *bmap      // 下一个可用的溢出桶地址
}

bmap

在源码中,bmap类型只有一个tophash字段。但在编译时期,Go编译器会根据用户代码自动注入相应的key,value等结构

表面的bmap

type bmap struct {
	// tophash generally contains the top byte of the hash value
	// for each key in this bucket. If tophash[0] < minTopHash,
	// tophash[0] is a bucket evacuation state instead.
	tophash [bucketCnt]uint8
	// Followed by bucketCnt keys and then bucketCnt elems.
	// NOTE: packing all the keys together and then all the elems together makes the
	// code a bit more complicated than alternating key/elem/key/elem/... but it allows
	// us to eliminate padding which would be needed for, e.g., map[int64]int8.
	// Followed by an overflow pointer.
}

实际的bmap

// 编译期间会动态地创建一个新的结构:
type bmap struct {
   topbits  [8]uint8   // 这里存储哈希值的高八位,用于在确定key的时候快速试错,加快增删改查寻址效率,有时候也叫tophash
   keys     [8]keytype   // 存储key的数组,这里bmap最多存储8个键值对
   elems   [8]valuetype    // 存储value的数组,这里bmap也最多存储8个键值对
   ...
   overflow uintptr     // 溢出桶指针
}

map底层图解

在这里插入图片描述

map的扩容

在go语言中,map的扩容是自动进行的,用于维护map的性能

首先,map在写入时会通过runtime.mapassign判断是否需要扩容

func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...
 // If we hit the max load factor or we have too many overflow buckets,
 // and we're not already in the middle of growing, start growing.
 if !h.growing() && (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
  hashGrow(t, h)
  goto again // Growing the table invalidates everything, so try again
 }
...
}

// overLoadFactor reports whether count items placed in 1<<B buckets is over loadFactor.
func overLoadFactor(count int, B uint8) bool {
 return count > bucketCnt && uintptr(count) > loadFactorNum*(bucketShift(B)/loadFactorDen)
}

func tooManyOverflowBuckets(noverflow uint16, B uint8) bool {
 if B > 15 {
  B = 15
 }
 return noverflow >= uint16(1)<<(B&15)
}

根据上面代码判断扩容有下面两个条件:

  • 负载因子超过阈值6.5:overLoadFactor(h.count+1, h.B) , 负载因子 = 元素数量÷桶数量
  • 使用了太多溢出桶(超出32768):tooManyOverflowBuckets(h.noverflow, h.B))

扩容方式:

  • 增量扩容:

当负载因子过大时,就新建一个bucket,新的bucket长度是原来的2倍,然后旧bucket数据搬迁到新的bucket。

  • 等量扩容

数据不多,但是溢出桶太多。扩容时buckets数量不变,重新做一遍类似增量扩容的搬迁动作,把松散的键值对重新排列一次,以使bucket的使用率更高,进而保证更快的存取

扩容步骤:

  • 1.新桶数组:新建一个大小为原来两倍的新的桶数组,map会标记为扩容状态。
func hashGrow(t *maptype, h *hmap) {
 ...
 // 原有桶设置给oldbuckets
 oldbuckets := h.buckets   
 // 创建新桶
 newbuckets, nextOverflow := makeBucketArray(t, h.B+bigger, nil)

 flags := h.flags &^ (iterator | oldIterator)
 if h.flags&iterator != 0 {
  flags |= oldIterator
 }
 // commit the grow (atomic wrt gc)
 h.B += bigger
 h.flags = flags
 h.oldbuckets = oldbuckets
 h.buckets = newbuckets
 h.nevacuate = 0
 h.noverflow = 0

 ...
}

  • 2.重新哈希:用oldbuckets指向原来的桶数组,buckets指向新的桶数组,遍历旧的桶数组中的所有键值对,并使用哈希函数重新计算每个键的位置,将它们插入到新的桶数组中。
// 这个是mapdelete函数中的处理迁移的位置
func mapassign(t *maptype, h *hmap, key unsafe.Pointer) unsafe.Pointer {
...
 if h.growing() {
  // 
  growWork(t, h, bucket)
 }
...
}

  • 3.逐步迁移:为了避免在扩容时暂停整个程序,Go的Map实现可能会选择渐进式驱逐进行迁移键值对。这意味着在扩容期间,旧的桶数组和新的桶数组会同时存在,新插入的键值对会直接放入新的桶中,而对旧桶的访问会触发迁移操作。
// 进入后是一个简单的判断,之后的evacuate是核心逻辑处理,特别多,感兴趣自己看源码
func growWork(t *maptype, h *hmap, bucket uintptr) {
	// make sure we evacuate the oldbucket corresponding
	// to the bucket we're about to use
	evacuate(t, h, bucket&h.oldbucketmask())

	// evacuate one more oldbucket to make progress on growing
	if h.growing() {
		evacuate(t, h, h.nevacuate)
	}
}

  • 4.更新内部状态:当oldbuckets中的键值对全部搬迁完毕后,Map的内部状态会更新,删除oldbuckets。

4.使用场景

  • 1.快速查找:当需要快速根据键查找值时,Map提供了平均时间复杂度为O(1)的查找性能。
  • 2.去重:当需要存储唯一键时,Map的键不允许重复,自然可以实现去重功能。
  • 3.动态集合:当需要动态地添加或删除键值对时,Map提供了灵活的操作。
  • 4.关联数据:当数据以键值对的形式存在,并且需要经常更新或查询时,Map是一个很好的选择。

5.使用建议

  1. 预分配:尽量使用make函数对已知大小的map分配容量
  2. 数据类型选择:使用较大的数据类型,如intint64
  3. 指针存值:对数据量大的结构体或者变量尽量使用指针传值存值
  4. 并发控制:对于并发访问,使用sync.Map或自行实现的并发安全Map。

6.参考资料

  • https://cloud.tencent.com/developer/article/2400014
  • https://blog.csdn.net/qq_35289736/article/details/137480760/
  • https://zhuanlan.zhihu.com/p/675715169/

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

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

相关文章

按下快门前的算法——对焦

对焦算法可以分为测距式&#xff0c;相位式&#xff0c;反差式。 其中测距式是通过激光&#xff0c;&#xff08;TOF&#xff0c;Time of Flight&#xff09;等主动式地得知物距&#xff0c;然后对焦。更常用的是后两者。 反差式CDAF&#xff08;Contrast Detection Auto Foc…

微调及代码

一、微调&#xff1a;迁移学习&#xff08;transfer learning&#xff09;将从源数据集学到的知识迁移到目标数据集。 二、步骤 1、在源数据集&#xff08;例如ImageNet数据集&#xff09;上预训练神经网络模型&#xff0c;即源模型。 2、创建一个新的神经网络模型&#xff…

【Python实战因果推断】31_双重差分2

目录 Canonical Difference-in-Differences Diff-in-Diff with Outcome Growth Canonical Difference-in-Differences 差分法的基本思想是&#xff0c;通过使用受治疗单位的基线&#xff0c;但应用对照单位的结果&#xff08;增长&#xff09;演变&#xff0c;来估算缺失的潜…

PostgreSQL 中如何处理数据的批量更新和事务日志管理?

文章目录 PostgreSQL 中数据的批量更新和事务日志管理 PostgreSQL 中数据的批量更新和事务日志管理 在数据库的世界里&#xff0c;数据的批量更新和事务日志管理就像是一场精心编排的舞蹈&#xff0c;需要精准的步伐和协调的动作。对于 PostgreSQL 而言&#xff0c;这两个方面…

LLM基础模型系列:Fine-Tuning总览

由于对大型语言模型&#xff0c;人工智能从业者经常被问到这样的问题&#xff1a;如何训练自己的数据&#xff1f;回答这个问题远非易事。生成式人工智能的最新进展是由具有许多参数的大规模模型驱动的&#xff0c;而训练这样的模型LLM需要昂贵的硬件&#xff08;即许多具有大量…

常见 Web漏洞分析与防范研究

前言&#xff1a; 在当今数字化时代&#xff0c;Web应用程序扮演着重要的角色&#xff0c;为我们提供了各种在线服务和功能。然而&#xff0c;这些应用程序往往面临着各种潜在的安全威胁&#xff0c;这些威胁可能会导致敏感信息泄露、系统瘫痪以及其他不良后果。 SQL注入漏洞 …

自主研发接口测试框架

测试任务&#xff1a;将以前完成的所有的脚本统一改写为unitest框架方式 1、需求原型 1.1 框架目录结构 V1.0&#xff1a;一般的设计思路分为配置层、脚本层、数据层、结果层&#xff0c;如下图所示 V 2.0&#xff1a;加入驱动层testdriver 1.2 框架各层需要完成的工作 1、配…

【CT】LeetCode手撕—70. 爬楼梯

目录 题目1- 思路2- 实现⭐70. 爬楼梯——题解思路 3- ACM实现 题目 原题连接&#xff1a;70. 爬楼梯 1- 思路 思路 爬楼梯 ——> 动规五部曲 2- 实现 ⭐70. 爬楼梯——题解思路 class Solution {public int climbStairs(int n) {if(n<1){return 1;}// 1. 定义 dp 数…

html5——CSS基础选择器

目录 标签选择器 类选择器 id选择器 三种选择器优先级 标签指定式选择器 包含选择器 群组选择器 通配符选择器 Emmet语法&#xff08;扩展补充&#xff09; 标签选择器 HTML标签作为标签选择器的名称&#xff1a; <h1>…<h6>、<p>、<img/> 语…

数据平滑处理(部分)

一、 移动平均&#xff08;Moving Average&#xff09; 是一种最简单的数据平滑方法&#xff0c;用于平滑时间序列数据。它通过计算一定窗口内数据点的平均值来减少噪音&#xff0c;同时保留数据的趋势。移动平均包括简单移动平均&#xff08;SMA&#xff09;或指数加权移动平均…

初始网络知识

前言&#x1f440;~ 上一章我们介绍了使用java代码操作文件&#xff0c;今天我们来聊聊网络的一些基础知识点&#xff0c;以便后续更深入的了解网络 网络 局域网&#xff08;LAN&#xff09; 广域网&#xff08;WAN&#xff09; 路由器 交换机 网络通信基础 IP地址 端…

可观察性优势:掌握当代编程技术

反馈循环是我们开发人员工作的关键。它们为我们提供信息&#xff0c;并让我们从用户过去和现在的行为中学习。这意味着我们可以根据过去的反应进行主动开发。 TestComplete 是一款自动化UI测试工具&#xff0c;这款工具目前在全球范围内被广泛应用于进行桌面、移动和Web应用的…

“闭门造车”之多模态思路浅谈:自回归学习与生成

©PaperWeekly 原创 作者 | 苏剑林 单位 | 科学空间 研究方向 | NLP、神经网络 这篇文章我们继续来闭门造车&#xff0c;分享一下笔者最近对多模态学习的一些新理解。 在前文《“闭门造车”之多模态思路浅谈&#xff1a;无损》中&#xff0c;我们强调了无损输入对于理想的…

压缩文件的解析方式

我们常用的压缩文件有两种&#xff1a;后缀为.zip或者.rar&#xff0c;接下来将介绍解析两种压缩文件的代码。需要用到三个jar包&#xff1a;commons-io-2.16.1.jar、junrar-7.5.5.jar、slf4j-api-2.0.13.jar&#xff0c;可以在官网下载&#xff0c;也可以发私信。 这段代码是一…

2.GAP:通用访问协议

GAP的简单理解 GAP这个名字&#xff0c;直接翻译过来不好理解。 简单点可以理解为&#xff1a; 这是蓝牙设备在互联之前&#xff0c;过程中&#xff0c;第一个用于交流的协议。在代码上&#xff0c;会给这个协议实现&#xff0c;连接参数的设置&#xff0c;连接事件的实现&am…

【算法】二叉树-迭代法实现前后中序遍历

递归的实现就是:每一次递归调用都会把函数的局部变量&#xff0c;参数值和返回地址等压入调用栈中&#xff0c;然后递归返回的时候&#xff0c;从栈顶弹出上一次递归的各项参数&#xff0c;这就是递归为什么可以返回上一层位置的原因 可以用栈实现二叉树的前中后序遍历 1. 前序…

【数学趣】拉窗帘模型之求面积引发的6个解法

抖音上推了一个趣题 题 求橙色部分的面积 蓝色部分是2个正方形。大的正方形边长为6。&#xff08;小的正方形一半被一个黄色三角形遮住了一半&#xff09; 答案 18 解法1&#xff1a;拉窗帘 先写一个代号&#xff0c;方便证明&#xff0c;H G 代表正方形。&#xff08;G…

AV1 编码标准中帧内预测技术详细说明

AV1 编码标准帧内预测 AV1&#xff08;AOMedia Video 1&#xff09;是一种开源的视频编码格式&#xff0c;旨在提供比现有标准更高的压缩效率和更好的视频质量。在帧内预测方面&#xff0c;AV1相较于其前身VP9和其他编解码标准&#xff0c;如H.264/AVC和H.265/HEVC&#xff0c;…

暑假第一次作业

第一步&#xff1a;给R1,R2,R3,R4配IP [R1-GigabitEthernet0/0/0]ip address 192.168.1.1 24 [R1-Serial4/0/0]ip address 15.0.0.1 24 [R2-GigabitEthernet0/0/0]ip address 192.168.2.1 24 [R2-Serial4/0/0]ip address 25.0.0.1 24 [R3-GigabitEthernet0/0/0]ip address 192.…

【Mutilism用74ls192和与非门设计3进制24进制加法计数器2荔枝】2022-5-10

缘由【数电 数字逻辑】如何用74ls192和与非门设计任意进制加法计数器&#xff1f;-嵌入式-CSDN问答