Go语言八股文之Map详解

💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。

非常期待和您一起在这个小小的网络世界里共同探索、学习和成长。💝💝💝 ✨✨ 欢迎订阅本专栏 ✨✨

前言

小郑最近在准备Go语言的面试题,通过github和b站等各种学习网站上学习go语言的八股文,并且整理出自己觉得面试可能会问到的知识点,希望通过做笔记的方式来巩固自己的知识点,并且也希望可以帮助到大家在面试的时候更加得心应手一些,那么从现在开始,和我一起加入八股学习之旅吧!

1.什么类型可以作为map 的key

在Go语言中,map的key可以是任何可以比较 的类型。这包括所有的基本类型,如整数、浮点数、字符串和布尔值,以及结构体和数组,只要它们没有被定义为包含不可比较的类型(如切片、映射或函数)。

注意,切片、映射和函数类型是不可比较的,因此不能作为map的key。如果你需要一个包含这些类型的key,你可以考虑使用一个指向这些类型的指针,或者将它们封装在一个可比较的结构体中,并确保结构体不包含任何不可比较的类型。


2.map 使用注意的点,是否并发安全?

2.1map使用的注意点

  1. key的唯一性:map中的每个key必须是唯一的。如果尝试使用已存在的key插入新值,则会覆盖旧值。
  2. key的不可变性:作为key的类型必须是可比较的,这通常意味着它们应该是不可变的。例如,在Go语言中,切片、映射和函数类型因为包含可变状态,所以不能直接作为map的key。
  3. 初始化和nil map :在Go语言中,声明一个map变量不会自动初始化它。未初始化的map变量的零值是nil,对nil map进行读写操作会引发panic。因此,在使用map之前,应该使用<font style="color:rgb(5, 7, 59);">make</font>函数进行初始化。
  4. 遍历顺序:map的遍历顺序是不确定的,每次遍历的结果可能不同。如果需要按照特定顺序处理map中的元素,应该先对key进行排序。
  5. 并发安全性:默认情况下,map并不是并发安全的。在并发环境下对同一个map进行读写操作可能会导致竞态条件和数据不一致性。

2.2并发安全性

  • Go语言中的map类型并不是并发安全的。这意味着,如果有多个goroutine尝试同时读写同一个map,可能会导致竞态条件和数据损坏。
  • 为了在并发环境下安全地使用map,可以采取以下几种策略:
    1. 使用互斥锁(sync.Mutex):在读写map的操作前后加锁,确保同一时间只有一个goroutine可以访问map。
    2. 使用读写互斥锁(sync.RWMutex):如果读操作远多于写操作,可以使用读写锁来提高性能。读写锁允许多个goroutine同时读取map,但在写入时需要独占访问。
    3. 使用并发安全的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内存标记与垃圾回收

  1. 删除操作 :使用<font style="color:rgb(5, 7, 59);background-color:rgb(253, 253, 254);">delete</font>函数从map中删除一个key时,该key及其关联的值会被从map的内部数据结构中移除。此时,这些值在逻辑上不再属于map的一部分。
  2. 内存标记:删除操作后,如果没有任何其他变量或数据结构引用被删除的值,那么这些值就变成了垃圾回收器的目标。Go语言的垃圾回收器(Garbage Collector, GC)会定期扫描内存,标记那些不再被使用的内存区域。
  3. 内存释放:在垃圾回收过程中,被标记为垃圾的内存区域会被释放回堆内存,供后续的内存分配使用。然而,这个过程并不是立即发生的,而是由垃圾回收器的触发条件和回收策略决定的。

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 的一些关键原理:

  1. 读写分离sync.Map 通过读写分离来提升性能。它内部维护了两种数据结构:一个只读的只读字典 (read),一个读写字典 (dirty)。读操作优先访问只读字典,只有在只读字典中找不到数据时才会访问读写字典。
  2. 延迟写入 :写操作并不立即更新只读字典(read),而是更新读写字典 (dirty)。只有在读操作发现只读字典的数据过时(即 misses 计数器超过阈值)时,才会将读写字典中的数据同步到只读字典。这种策略减少了写操作对读操作的影响。
  3. 原子操作 :读操作大部分是无锁的,因为它们主要访问只读的 read map,并通过原子操作 (atomic.Value) 来保护读操作;写操作会加锁(使用 sync.Mutex)保护写操作,以确保对 dirty map 的并发安全 ,确保高并发环境下的安全性。
  4. 条目淘汰:当一个条目被删除时,它只从读写字典中删除。只有在下一次数据同步时,该条目才会从只读字典中删除。

通过这种设计,sync.Map 在读多写少的场景下能够提供较高的性能,同时保证并发安全。

9.sync.map的锁机制跟你自己用锁加上map有区别么

sync.Map 的锁机制和自己使用锁(如 sync.Mutexsync.RWMutex)加上 map 的方式有一些关键区别:

自己使用锁和 map

  1. 全局锁
    • 你需要自己管理锁,通常是一个全局的 sync.Mutexsync.RWMutex
    • 对于读多写少的场景,使用 sync.RWMutex 可以允许多个读操作同时进行,但写操作依然会阻塞所有读操作。
  1. 手动处理
    • 你需要自己编写代码来处理加锁、解锁、读写操作。
    • 错误使用锁可能导致死锁、竞态条件等问题。
  1. 简单直观
    • 实现简单,容易理解和调试。

**<u>sync.Map</u>**

  1. 读写分离
    • sync.Map 内部使用读写分离的策略,通过只读和读写两个 map 提高读操作的性能。
    • 读操作大部分情况下是无锁的,只有在只读 map 中找不到数据时,才会加锁访问读写 map。
  1. 延迟写入
    • 写操作更新读写 map(dirty),但不会立即更新只读 map(read)。只有当读操作发现只读 map 中的数据过时时,才会将读写 map 的数据同步到只读 map 中。
  1. 内置优化
    • sync.Map 内部有各种优化措施,如原子操作、延迟写入等,使得它在读多写少的场景下性能更高。

区别总结

  • 并发性能sync.Map 通过读写分离和延迟写入在读多写少的场景下提供更高的并发性能,而使用全局锁的 map 在读写频繁时性能较低。
  • 复杂性和易用性sync.Map 封装了复杂的并发控制逻辑,使用起来更简单,而自己管理锁和 map 需要处理更多的并发控制细节。
  • 适用场景**<font style="color:#DF2A3F;">sync.Map</font>**适用于读多写少的场景,而使用**全局锁的 map 适用于读写操作较均衡或者对性能要求不高**的场景。

如果你的应用场景是读多写少且对性能要求较高,sync.Map 会是一个更好的选择。而对于简单的并发访问控制,使用 sync.Mutexsync.RWMutex 加上 map 也可以满足需求。

❤️❤️❤️小郑是普通学生水平,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

相关推荐
是垚不是土1 小时前
Go语言中的并发编程--详细讲解
java·运维·开发语言·算法·golang·运维开发
每次的天空8 小时前
Android第六次面试总结之Java设计模式(二)
android·java·面试
独行soc9 小时前
2025年渗透测试面试题总结-某战队红队实习面经(附回答)(题目+回答)
linux·运维·服务器·学习·面试·职场和发展·渗透测试
xyd陈宇阳12 小时前
嵌入式开发面试题详解:STM32 与嵌入式开发核心知识全面解析
stm32·单片机·嵌入式硬件·面试
试着13 小时前
【AI面试准备】TensorFlow与PyTorch构建缺陷预测模型
人工智能·pytorch·面试·tensorflow·测试
搞不懂语言的程序员16 小时前
Redis面试 实战贴 后面持续更新链接
数据库·redis·面试
uperficialyu16 小时前
2025年01月09日德美医疗前端面试
前端·面试
猫头虎17 小时前
多线程“CPU 飙高”问题:如何确保配置的线程数与CPU核数匹配(Java、GoLang、Python )中的最佳实践解决方案
java·python·缓存·golang·需求分析·极限编程·结对编程
独行soc17 小时前
2025年渗透测试面试题总结-拷打题库36(题目+回答)
linux·运维·服务器·网络安全·面试·职场和发展·渗透测试