首页 > 自考资讯 > 高考百科

Go 语言中的映射(Map)详解:键值对实际是如何存储的(go语言map的多键索引)

小条 2024-09-23

一、引言

如果您是Go 语言的新手,您可能会对如何在Go 中使用映射感到有点困惑。即使有经验,也可能很难理解映射的实际工作原理。

例如:

0c4a0f934887455492324b9e8b513d42~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=x86PAUpkwgURRyqF6uwrCDUfTes%3D 或者例如在映射中使用for-range循环时的排序问题。

在继续之前,请记住这里的信息—— 是基于Go 1.23 的。如果情况发生变化且此内容不再是最新内容,请私信我。

二、Go 语言中的映射:快速入门

1. 基本概念

Go 的Map 是用作键值存储的内置类型。与键递增索引的数组不同,映射的键可以是任何等效类型,从而赋予其更大的灵活性。

26aa1816399c4a22bb5b85463d91b36f~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=qRWBJpwm5gX4BwNUeKqJ0HQYyfQ%3D

2. 如何创建

使用make() 创建一个空映射并分配它。ed07470efb614763ad75f29331b27482~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=3O3mCtyTqYIjO5FMuFVksVRgbjE%3D 地图['a': 1,'b': 2]

键值对是在创建时通过映射文字直接设置的。44dc5b34ea6a45ceba6ab28222e0486e~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=MiDcSy33lvGtkOycodhYCkf6xhk%3D3.删除操作

如果要删除键值对,可以使用删除函数delete(m, 'a')。

4. 零值和特殊情况

该map的零值为nil,相当于一个空map。搜索不存在的键将返回“零”作为值类型。但是,您无法将新的键值对添加到nil 映射中。

860d099384dd46deafaafb3b851c8e69~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=8lMWgksNycyqrtNX2eUYvZoGRnY%3D 对于nil 映射遍历,不会引发错误,但不会执行操作。

5.其他特点

for-range 循环返回的键没有特定的顺序。映射不是线程安全的。您可以使用ok: _, ok :=m[key] 来检查该键是否在地图中。 6. 关于映射键

可比较类型:允许使用==比较相同类型的两个值的类型被视为可比较类型。但是,某些类型(例如切片、函数或包含切片和映射的结构)不能用作映射键。接口:接口可能具有可比性,也可能不具有可比性。使用空接口作为键时要小心,因为这可能会导致运行时错误。841d80e62d8842909e779a4138224728~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=Js8CRvB6Z0BF%2F9bpctPlsKZ0%2B88%3D 运行时错误“hash of unhashable type []int”还揭示了有关Go 如何在幕后处理映射的线索。

三、映射(Map)剖析

1. 映射的基本结构

在Go 代码中,单个映射是一种抽象,隐藏了数据组织的复杂细节,由称为存储桶的单元组成。

a07fcf52c3d7483c9a2dd77d8878197e~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=rV%2B1QjTWub%2FsacXNMqWJN%2B3APGk%3D映射包含一个指向桶数组的指针,以便在分配或传递映射时相关变量和函数参数可以共享相同的指针。

2. 传输和更改映射

94c38f029a2349e780598e42e7b94763~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=%2FPRFm8Ab0CFpXYXYgLB2FYAXsoQ%3D 注意,映射的底层指向hmap的指针,但它不是引用类型。对整个映射的更改不会影响原始映射。

72e1a445bab74d08ace3292aef6c46fc~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=tmMf%2BBY9IFG4v0GJnlnUDBh73Zg%3D在Go中,一切都是按值传递的。实际发生的情况有些不同。当你将map m1传递给changeMap函数时,Go会复制*hmap结构。所以main()中的m1和changeMap()函数中的m2在技术上是指向同一个hmap的不同指针。

6244a3e7426844a28acd62337c744e25~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=1fXFPr3sdWN8xgYqcmeky3DByKE%3D 映射是值类型

3、铲斗特点

每个桶最多可以容纳8 个键值对。

c27bda259c304af397e72a90c78026af~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=XZgUmQPlCVP%2BdefS20p%2BoWRuRTo%3D 映射桶

上面的map有两个bucket,len(map)是6。

4. 简单的赋值场景

让我们看一下下图中的一个简单的分配场景。我有一个空地图,我想为其分配键值对'hello' : 1。

5f05ccba203b44f1b672aa1173b306ed~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=BiiNoI9KAhGWq6W%2FYZo4Xo6kPos%3D 将键值对分配给空映射

首先将“hello”哈希为一个数字,然后对桶的数量取模。由于这里只有一个桶,所有模1的数字都是0,直接进入桶0。添加另一个键值对时会发生相同的过程,如果存储桶0 已满或键不同,我们将移动到下一个槽。

5. 哈希和循环中的关键顺序

14的顶端有4个铲斗,这就是有效载荷系数。还记得负载因子阈值是6.5 吗?它直接影响地图何时增长。

Tip 13 有两个桶,负载系数为13/2=6.5,达到但不超过阈值。如果将其增加到14,负载因子将超过6.5 并且将开始增长。

同样,如果有26个芯片和4个桶,则负载系数为26/4=6.5,再次达到阈值。超过26 个时,地图必须扩展以容纳更多元素。

基本上,从第二个范围开始,铲尖范围将比前一个范围增加一倍,并且铲斗的数量和容量将相似。

四、增长时的迁移

1. 增加映射带来的问题

绘制增长图回答了两个常见问题:

“为什么我无法获取映射元素的地址?”“为什么在不同点不能保证for-range 的顺序?”

当映射增长时,会分配一个新的桶数组,其大小是旧数组的两倍,因此旧桶中的条目位置无效,必须移动到新桶中。

0ec21d5d42ac47efae9cabbfbb0c2065~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=jaIT8%2BuTPoSsTtfRhgFDMxCD1T4%3D 映射迁移

如果您的映射有大量键值对(例如1000 个),则一次移动所有键可能会很昂贵并且会阻塞goroutine。为了避免这种情况,Go 使用“增量增长”来分散操作并保持程序平稳运行。

这个过程很复杂,需要管理新旧存储桶,同时保持映射完整性。

3. 渐进式增长的触发条件

只有两种情况会导致逐渐增长。

将键值对分配给映射。从地图上删除一个关键点。每个操作都会触发一个迁移过程,将至少一个存储桶移动到新的存储桶阵列。

当执行m['Hello']=2操作并且map增长时,包含'Hello'键的旧桶将首先被迁移。

2a67263ec2fa4db5b9a761d35faedb70~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=3%2FBM%2BJ8DUVqb6XHLE8%2B%2Fm64%2FxS4%3D 键“Hello”可以移动到两个新存储桶之一

将旧桶元素移动到新桶的规则:

例如,从4个桶到8个桶,旧的“桶1”元素被移动到新的“桶1”或新的“桶5”。

Hash % 4==1 表示hash % 8==1 或hash % 8==5。因为在旧桶中,H % 4==1,所以H的最后两位是01。考虑新桶数组的最后3 位:

如果第三位(从右)是0,最后三位是001,则表示H % 8==1。如果第三位(从右起)是1,最后三位是010,这意味着H % 8==5。47549856d44c4d4f81166dd695e29d60~noop.image?_iz=58558&from=article.pc_detail&lk3s=953192f4&x-expires=1727657043&x-signature=F15Y0jS5U6LwOzLxeJL5B9HjFW4%3D 如何迁移旧桶

如果旧桶有溢出桶,则将元素移动到新桶,所有元素都移动完毕后,旧桶被“top hash”字段标记为“已迁移”。

五、更多

掌握Golang精髓,释放你的编程潜力!关注我的专栏《Golang实用技巧》。您将发现生产中的最佳实践,并探索有关高并发编程的实用教程。从分享实用的Golang技巧到深入分析真实的应用场景,你可以成为真正的Golang高手。无论您是初学者还是经验丰富的开发人员,您都会找到所需的灵感和知识。让我们一起探索Golang的无限可能。

版权声明:本文转载于网络,版权归作者所有。如有侵权,请联系本站编辑删除。

猜你喜欢