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 也可以满足需求。

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

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

相关推荐
Lee川5 小时前
优雅进化的JavaScript:从ES6+新特性看现代前端开发范式
javascript·面试
Lee川9 小时前
从异步迷雾到优雅流程:JavaScript异步编程与内存管理的现代化之旅
javascript·面试
晴殇i11 小时前
揭秘JavaScript中那些“不冒泡”的DOM事件
前端·javascript·面试
绝无仅有11 小时前
Redis过期删除与内存淘汰策略详解
后端·面试·架构
绝无仅有11 小时前
Redis大Key问题排查与解决方案全解析
后端·面试·架构
AAA梅狸猫12 小时前
Looper.loop() 循环机制
面试
AAA梅狸猫12 小时前
Handler基本概念
面试
Wect13 小时前
浏览器缓存机制
前端·面试·浏览器
掘金安东尼13 小时前
Fun with TypeScript Generics:玩转 TS 泛型
前端·javascript·面试
掘金安东尼13 小时前
Next.js 企业级落地
前端·javascript·面试