掌握Map的11个常见问题,轻松学会Go语言!

1. Map 使用时需要注意哪些问题?

  • Map 的键必须是可比较的类型,如整数、字符串和指针等,但是切片、函数和结构体等类型是不可比较的,因此不能用作键。
  • Map 中的元素是无序的,这意味着遍历 Map 时,元素的顺序可能会随机改变。
  • Map 的容量是动态变化的,它会自动调整容量以适应新的元素。
  • 如果使用未初始化的 Map,会导致运行时错误。需要使用 make() 函数来初始化 Map。
  • Map 在并发环境下不是安全的。如果需要在并发环境下使用 Map,需要使用 sync 包中提供的锁机制来保护 Map。

2. Map 扩容是怎么实现的?

在 Go 中,Map 的内部实现是基于哈希表(hash table)的,因此,当 Map 中的键值对数量增加时,Map 会自动扩容以提供更多的存储空间。下面是 Go Map 扩容的具体步骤:

  • 当 Map 中的元素数量超过了负载因子(load factor)和哈希表容量的乘积时,map 就会触发扩容操作。在 Go 中,负载因子默认为 6.5。
  • Go Map 在扩容时会创建一个新的哈希表,并将原来的键值对重新散列到新的哈希表中。为了减少哈希冲突,新哈希表的容量是原来的两倍,并且容量一定是 2 的幂次方。
  • 在重新散列过程中,Go Map 会根据哈希值将原来的键值对分配到新哈希表中的对应位置上。如果两个键值对的哈希值相同,会使用链式哈希表(chained hash table)的方式进行处理,将它们放在同一个桶(bucket)中。
  • 一旦所有的键值对都已经重新散列到新的哈希表中,Go Map 就会将原来的哈希表释放掉,将新的哈希表作为 Map 的内部存储结构。

注意:  由于扩容操作可能会导致大量的重新散列操作,因此在扩容时可能会造成一定的性能影响。为了避免频繁扩容,可以在创建 Map 时指定一个较大的容量,或者手动调用 runtime.GC()  来触发垃圾回收操作,以释放未使用的内存。

3. Map 的 panic 能被 recover 吗?

不能,并发读写 Map 也是很容易遇到的问题。如下代码可以验证:

输出:

fatal error: concurrent map read and map write

goroutine 18 [running]:
runtime.throw({0x1021bdf62?, 0x0?})
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/panic.go:992 +0x50 fp=0x14000046f70 sp=0x14000046f40 pc=0x10215f050
runtime.mapaccess1_fast64(0x0?, 0x0?, 0x1)
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/map_fast64.go:22 +0x174 fp=0x14000046f90 sp=0x14000046f70 pc=0x10213eaa4
main.foo.func2()
        /Users/xxx/go/learn/main.go:43 +0x50 fp=0x14000046fd0 sp=0x14000046f90 pc=0x1021b8a00
runtime.goexit()
        /opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/asm_arm64.s:1270 +0x4 fp=0x14000046fd0 sp=0x14000046fd0 pc=0x10218aa64
created by main.foo
        /Users/xxx/go/learn/main.go:36 +0x84

goroutine 1 [runnable]:
main.foo()
        /Users/xxx/go/learn/main.go:47 +0x98
main.main()
        /Users/xxx/go/learn/main.go:52 +0x20
exit status 2

exit/opt/homebrew/Cellar/go@1.18/1.18.10/libexec/src/runtime/map_fast64.go:22

小结一下:

runtime.throw()recovererrorsync.Mutexsync.RWMutextsync.Map

4. 并发使用 Map 除了加锁还有什么其他方案吗?

除了加锁之外,Go 并发使用 Map 的其他常见解决方案包括使用 sync.Map 和使用并发安全的第三方 Map 库。

1、使用 sync.Map sync.Map 是 Go 1.9 新增的一种并发安全的 Map,它的读写操作都是并发安全的,无需加锁。使用 sync.Map 的示例代码如下:

2、使用并发安全的第三方 Map 库 除了使用 sync.Map,还可以使用其他第三方的并发安全的 Map 库,如 concurrent-map、ccmap 等。这些库的使用方式与 Go 标准库的 Map 类似,但它们都提供了更高效、更可靠的并发安全保证。

注意:  使用并发安全的第三方 Map 库可能会导致额外的依赖和复杂性,因此需要仔细评估是否值得使用。

5. sync.Map 和加锁的区别是什么?

  • sync.Map 和使用锁的区别在于,sync.Map 不需要在并发访问时进行加锁和解锁操作。相比之下,使用锁需要在并发访问时显式地加锁和解锁,以避免竞争条件和数据竞争问题。
  • 在使用锁时,当多个 goroutine 同时访问同一块数据时,必须通过加锁来避免竞争条件。这意味着只有一个 goroutine 能够访问该数据,并且在该 goroutine 完成工作后,其他 goroutine 才能够访问数据。这种方式可以确保数据的一致性,但是加锁会带来额外的开销,并且在高并发情况下可能会影响性能。
  • 相比之下,sync.Map 使用了更高级的算法来避免竞争条件和数据竞争问题,而不需要显式地进行加锁和解锁。当多个 goroutine 同时访问 sync.Map 时,它会自动分配不同的段来存储数据,并且每个段都有自己的读写锁,以避免竞争条件。这种方式可以提高并发性能,减少开销,并且避免死锁等问题。
sync.Map

6. Map 的查询时间复杂度?

Go 中的 Map 底层实现采用了哈希表,因此其查询时间复杂度为 O(1),最坏情况为 O(n)。

7.Map Rehash 的策略是怎样的?什么时机会发生 Rehash?

当哈希表中的元素数量达到一定阈值时,就会触发哈希表的扩容操作,也就是进行 rehash。

Go 标准库中的哈希表实现在以下情况下会触发 rehash 操作:

  • 当哈希表中的元素数量超过了哈希表容量的 2/3 时,会触发扩容操作。此时,哈希表的容量会翻倍,并且哈希表中所有的元素都会重新哈希到新的槽位中。
  • 当哈希表中的元素数量过少,而哈希表的容量过大时,会触发收缩操作。此时,哈希表的容量会减半,并且哈希表中所有的元素都会重新哈希到新的槽位中。
  • 当哈希表的探测序列过长时,会触发重排操作。此时,哈希表中的元素不会重新哈希,但是它们的存储位置会发生变化,从而减少聚簇现象,提高哈希表的性能。

在进行 rehash 操作时,哈希表会创建一个新的数组来存储重新哈希后的元素,然后将旧数组中的元素逐个复制到新数组中。由于重新哈希的过程比较耗时,因此 Go 标准库中的哈希表实现采用了增量式 rehash 策略,在扩容和收缩时只会处理一部分元素,避免一次性处理过多元素导致性能下降。具体来说,增量式 rehash 的策略是:

  • 将新数组的容量设置为旧数组的两倍或一半,并且将哈希表的增量计数器加一。
  • 在对哈希表进行操作时,如果发现增量计数器的值达到了一个阈值,就会开始进行增量式 rehash 操作,将一部分元素从旧数组中复制到新数组中,并且重新计算这些元素的哈希值。
  • 在完成一次增量式 rehash 操作后,会将哈希表的增量计数器清零。

通过增量式 rehash 的策略,Go 标准库中的哈希表实现可以在保证较短的 rehash 时间的同时,避免一次性处理过多元素导致性能下降。

8. Map Rehash 具体会影响什么?哈希结果会受到什么影响?

Map
Map

9. Map Rehash 过程中存放在旧桶的元素如何迁移?

rehash 操作会创建一个新的哈希表,同时保留旧的哈希表不变。然后,它会依次遍历旧哈希表中的每个桶,将桶中的所有键值对复制到新哈希表中对应的桶中。在遍历每个桶时,如果桶中的元素已经被删除,则跳过这些元素。

Map
MapMap
MapMapMap

10. sync.Map 的 Load() 方法流程?

sync.MapLoad()
Load()sync.Mapmapsync.MapLoad()Load()sync.MapLoad()Load()sync.Mapmap
sync.Mapmapmap

11. sync.Map Store() 如何保持缓存层和底层 Map 数据是相同的?

sync.MapStore()mapsync.Map
Store()sync.Mapinterface{}mapsync.Mapmapmapmapmapsync.Mapmapsync.MapmapmapStore()mapmapmapsync.Mapmap
sync.Mapmapmap