Go语言八股文之Map详解
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。
非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨
前言
小郑最近在准备Go语言的面试题,通过github和b站等各种学习网站上学习go语言的八股文,并且整理出自己觉得面试可能会问到的知识点,希望通过做笔记的方式来巩固自己的知识点,并且也希望可以帮助到大家在面试的时候更加得心应手一些,那么从现在开始,和我一起加入这趟八股学习之吧!
1.什么类型可以作为map 的key
在Go语言中,map的key可以是任何可以比较的类型。这包括所有的基本类型,如整数、浮点数、字符串和布尔值,以及结构体和数组,只要它们没有被定义为包含不可比较的类型(如切片、映射或函数)。
注意,切片、映射和函数类型是不可比较的,因此不能作为map的key。如果你需要一个包含这些类型的key,你可以考虑使用一个指向这些类型的指针,或者将它们封装在一个可比较的结构体中,并确保结构体不包含任何不可比较的类型。
2.map 使用注意的点,是否并发安全?
2.1map使用的注意点
- key的唯一性:map中的每个key必须是唯一的。如果尝试使用已存在的key插入新值,则会覆盖旧值。
- key的不可变性:作为key的类型必须是可比较的,这通常意味着它们应该是不可变的。例如,在Go语言中,切片、映射和函数类型因为包含可变状态,所以不能直接作为map的key。
- 初始化和nil map:在Go语言中,声明一个map变量不会自动初始化它。未初始化的map变量的零值是nil,对nil map进行读写操作会引发panic。因此,在使用map之前,应该使用
<font style="color:rgb(5, 7, 59);">make</font>
函数进行初始化。 - 遍历顺序:map的遍历顺序是不确定的,每次遍历的结果可能不同。如果需要按照特定顺序处理map中的元素,应该先对key进行排序。
- 并发安全性:默认情况下,map并不是并发安全的。在并发环境下对同一个map进行读写操作可能会导致竞态条件和数据不一致性。
2.2并发安全性
- Go语言中的map类型并不是并发安全的。这意味着,如果有多个goroutine尝试同时读写同一个map,可能会导致竞态条件和数据损坏。
- 为了在并发环境下安全地使用map,可以采取以下几种策略:
-
- 使用互斥锁(sync.Mutex):在读写map的操作前后加锁,确保同一时间只有一个goroutine可以访问map。
- 使用读写互斥锁(sync.RWMutex):如果读操作远多于写操作,可以使用读写锁来提高性能。读写锁允许多个goroutine同时读取map,但在写入时需要独占访问。
- 使用并发安全的map(sync.Map):从Go 1.9版本开始,标准库中的
<font style="color:rgb(5, 7, 59);">sync</font>
包提供了<font style="color:rgb(5, 7, 59);">sync.Map</font>
类型,这是一个专为并发环境设计的map。它提供了一系列方法来安全地在多个goroutine之间共享数据。
结论:
在使用map时,需要注意其key的唯一性和不可变性,以及初始化和并发安全性的问题。特别是在并发环境下,应该采取适当的措施来确保map的安全访问,以避免竞态条件和数据不一致性。在Go语言中,可以通过使用互斥锁、读写互斥锁或并发安全的map(<font style="color:rgb(5, 7, 59);">sync.Map</font>
)来实现这一点。
方案 | 实现复杂度 | 性能(读多写少) | 性能(写多) | 使用场景 |
sync.Mutex | 低 | 中等 | 中等 | 写操作频繁,对并发性能要求不高 |
sync.RWMutex | 中等 | 高 | 中等 | 读多写少,需要较高并发读性能 |
sync.Map | 低(API不同) | 高 | 中等偏下 | 读多写少,追求简洁的并发编程模型 |
3.map 循环是有序的还是无序的?
在Go语言中,map的循环(遍历)是无序的。这意味着当你遍历map时,每次遍历的顺序可能都不同。Go语言的map是基于哈希表的,因此元素的存储顺序是不确定的,并且可能会随着元素的添加、删除等操作而改变。
如果你需要按照特定的顺序处理map中的元素,你应该先将key提取到一个切片中,对切片进行排序,然后按照排序后的顺序遍历切片,并从map中取出对应的值。这样,你就可以按照特定的顺序处理map中的元素了。
4.map 中删除一个 key,它的内存会释放么?
在Go语言中,从map中删除一个key时,其内存释放的行为并非直观且立即的,这涉及到Go语言的内存管理机制。具体来说,删除map中的key后,其内存释放情况如下:
4.1内存标记与垃圾回收
- 删除操作:使用
<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">delete</font>
函数从map中删除一个key时,该key及其关联的值会被从map的内部数据结构中移除。此时,这些值在逻辑上不再属于map的一部分。 - 内存标记:删除操作后,如果没有任何其他变量或数据结构引用被删除的值,那么这些值就变成了垃圾回收器的目标。Go语言的垃圾回收器(Garbage Collector, GC)会定期扫描内存,标记那些不再被使用的内存区域。
- 内存释放:在垃圾回收过程中,被标记为垃圾的内存区域会被释放回堆内存,供后续的内存分配使用。然而,这个过程并不是立即发生的,而是由垃圾回收器的触发条件和回收策略决定的。
5.nil map 和空 map 有何不同?
在Go语言中,nil map和空map之间存在一些关键的不同点,主要体现在它们的初始状态、对增删查操作的影响以及内存占用等方面。
5.1初始状态与内存占用
- nil map:未初始化的map的零值是nil。这意味着map变量被声明后,如果没有通过
<font style="color:rgb(5, 7, 59);">make</font>
函数或其他方式显式初始化,它将保持nil状态。nil map不占用实际的内存空间来存储键值对,因为它没有底层的哈希表结构。 - 空map:空map是通过
<font style="color:#DF2A3F;">make</font>
函数或其他方式初始化但没有添加任何键值对的map。空map已经分配了底层的哈希表结构,但表中没有存储任何键值对。因此,空map占用了一定的内存空间,尽管这个空间相对较小。
5.2对增删查操作的影响
- nil map:
-
- 添加操作:向nil map中添加键值对将导致运行时panic,因为nil map没有底层的哈希表来存储数据。
- 删除操作:在早期的Go版本中,尝试从nil map中删除键值对也可能导致panic,但在最新的Go版本中,这一行为可能已经被改变(具体取决于Go的版本),但通常不建议对nil map执行删除操作。
- 查找操作:从nil map中查找键值对不会引发panic,但会返回对应类型的零值,表示未找到键值对。
- 空map:
-
- 添加操作:向空map中添加键值对是安全的,键值对会被添加到map中。
- 删除操作:从空map中删除键值对是一个空操作,不会引发panic,因为map中原本就没有该键值对。
- 查找操作:从空map中查找不存在的键值对也会返回对应类型的零值,表示未找到键值对。
6.map 的数据结构是什么?
map-地鼠文档
hmap 是 Go map 的顶层结构,包含多个 bucket。
bucket 是哈希表中的一个存储单元,存储多个 bmap。
bmap 是 Go 中存储键值对的最小单元,采用链表来解决哈希冲突。
golang 中 map 是一个 kv 对集合。底层使用 hash table,用链表来解决冲突 ,出现冲突时,不是每一个 key 都申请一个结构通过链表串起来,而是以 bmap 为最小粒度挂载,一个 bmap 可以放 8 个 kv。在哈希函数的选择上,会在程序启动时,检测 cpu 是否支持 aes,如果支持,则使用 aes hash,否则使用 memhash。每个 map 的底层结构是 hmap,是有若干个结构为 bmap 的 bucket 组成的数组。每个 bucket 底层都采用链表结构。
hmap 的结构如下
type hmap struct { count int // 元素个数 flags uint8 B uint8 // 扩容常量相关字段B是buckets数组的长度的对数 2^B noverflow uint16 // 溢出的bucket个数 hash0 uint32 // hash seed buckets unsafe.Pointer // buckets 数组指针 oldbuckets unsafe.Pointer // 结构扩容的时候用于赋值的buckets数组 nevacuate uintptr // 搬迁进度 extra *mapextra // 用于扩容的指针
}
bucket数据结构
type bmap struct {tophash [8]uint8 //存储哈希值的高8位data byte[1] //key value数据:key/key/key/.../value/value/value...overflow *bmap //溢出bucket的地址
}
7.可以对map里面的一个元素取地址吗
在Go语言中,你不能直接对map中的元素取地址,因为map的元素并不是固定的内存位置。当你从map中获取一个元素的值时,你实际上得到的是该值的一个副本,而不是它的实际存储位置的引用。这意味着,即使你尝试获取这个值的地址,你也只是得到了这个副本的地址,而不是map中原始元素的地址。
例如,考虑以下代码:
m := make(map[string]int)
m["key"] = 42
value := m["key"]
fmt.Println(&value) // 打印的是value变量的地址,而不是map中元素的地址
在这个例子中,<font style="color:rgb(5, 7, 59);">&value</font>
是变量 <font style="color:rgb(5, 7, 59);">value</font>
的地址,它包含了从map中检索出来的值的副本。如果你修改了 <font style="color:rgb(5, 7, 59);">value</font>
,map中的原始值是不会改变的。
如果你需要修改map中的值,你应该直接通过map的键来设置新的值:
m["key"] = newValue
这样,你就会直接修改map中存储的值,而不是修改一个副本。
如果你确实需要引用map中的值,并且希望这个引用能够反映map中值的改变,你可以使用指针类型的值作为map的元素。这样,你就可以存储和修改指向实际数据的指针了。例如:
m := make(map[string]*int)
m["key"] = new(int)
*m["key"] = 42
fmt.Println(*m["key"]) // 输出42
在这个例子中,map的值是指向int的指针,所以你可以通过指针来修改map中的实际值。
8.sync.map
sync.Map
是 Go 语言标准库中提供的并发安全的 Map 类型,它适用于读多写少的场景。以下是 sync.Map
的一些关键原理:
- 读写分离:
sync.Map
通过读写分离来提升性能。它内部维护了两种数据结构:一个只读的只读字典 (read
),一个读写字典 (dirty
)。读操作优先访问只读字典,只有在只读字典中找不到数据时才会访问读写字典。 - 延迟写入:写操作并不立即更新只读字典(
read
),而是更新读写字典 (dirty
)。只有在读操作发现只读字典的数据过时(即misses
计数器超过阈值)时,才会将读写字典中的数据同步到只读字典。这种策略减少了写操作对读操作的影响。 - 原子操作:读操作大部分是无锁的,因为它们主要访问只读的
read
map,并通过原子操作 (atomic.Value
) 来保护读操作;写操作会加锁(使用sync.Mutex
)保护写操作,以确保对dirty
map 的并发安全 ,确保高并发环境下的安全性。 - 条目淘汰:当一个条目被删除时,它只从读写字典中删除。只有在下一次数据同步时,该条目才会从只读字典中删除。
通过这种设计,sync.Map
在读多写少的场景下能够提供较高的性能,同时保证并发安全。
9.sync.map的锁机制跟你自己用锁加上map有区别么
sync.Map
的锁机制和自己使用锁(如 sync.Mutex
或 sync.RWMutex
)加上 map 的方式有一些关键区别:
自己使用锁和 map
- 全局锁:
-
- 你需要自己管理锁,通常是一个全局的
sync.Mutex
或sync.RWMutex
。 - 对于读多写少的场景,使用
sync.RWMutex
可以允许多个读操作同时进行,但写操作依然会阻塞所有读操作。
- 你需要自己管理锁,通常是一个全局的
- 手动处理:
-
- 你需要自己编写代码来处理加锁、解锁、读写操作。
- 错误使用锁可能导致死锁、竞态条件等问题。
- 简单直观:
-
- 实现简单,容易理解和调试。
**<u>sync.Map</u>**
- 读写分离:
-
sync.Map
内部使用读写分离的策略,通过只读和读写两个 map 提高读操作的性能。- 读操作大部分情况下是无锁的,只有在只读 map 中找不到数据时,才会加锁访问读写 map。
- 延迟写入:
-
- 写操作更新读写 map(
dirty
),但不会立即更新只读 map(read
)。只有当读操作发现只读 map 中的数据过时时,才会将读写 map 的数据同步到只读 map 中。
- 写操作更新读写 map(
- 内置优化:
-
sync.Map
内部有各种优化措施,如原子操作、延迟写入等,使得它在读多写少的场景下性能更高。
区别总结
- 并发性能:
sync.Map
通过读写分离和延迟写入在读多写少的场景下提供更高的并发性能,而使用全局锁的 map 在读写频繁时性能较低。 - 复杂性和易用性:
sync.Map
封装了复杂的并发控制逻辑,使用起来更简单,而自己管理锁和 map 需要处理更多的并发控制细节。 - 适用场景:
**<font style="color:#DF2A3F;">sync.Map</font>**
适用于读多写少的场景,而使用**全局锁的 map 适用于读写操作较均衡或者对性能要求不高**的场景。
如果你的应用场景是读多写少且对性能要求较高,sync.Map
会是一个更好的选择。而对于简单的并发访问控制,使用 sync.Mutex
或 sync.RWMutex
加上 map 也可以满足需求。
❤️❤️❤️小郑是普通学生水平,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄
💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍