likes
comments
collection
share

聊一聊 map

作者站长头像
站长
· 阅读数 17

map 是 go 中一个重要的数据结构, 可以通过 map 存储键值对「key-value」, 可以在常数时间内实现对 key 的查找。

使用

可以使用以下方式声明一个 map。

m, m1 := make(map[string]int), make(map[string]int, 8) // 分配默认空间 & 指定空间
var m2 map[string]int                                  // 并未创建空间
fmt.Println(m == nil, m1 == nil, m2 == nil)            // false false true

对于未分配空间 map 可以进行查找、删除操作, 但不能进行添加操作

var m map[int]int
fmt.Println(m[1]) // 0
delete(m, 1)
m[0] = 0
// panic: assignment to entry in nil map

对于 map 的查找, 如果不存在也会返回零值, 可以通过下面的方式判断 map 中是否包含了指定的 key

m := map[int]int{
   1: 1,
}
fmt.Println(m[0]) // 0
if _, ok := m[0]; !ok {
   fmt.Println("not contain key: 0")
}

map 传参, 可以将 map 看作指向一块空间的指针。因此, 当参数传递时, 会修改指向的内容

func main() {
    m := map[int]int{}
    paramMap(m)
    fmt.Println(m) // 输出更新之后的值
}

func paramMap(m map[int]int) {
   for i := 0; i < 100; i++ {
      m[i] = i
   }
}

map 的遍历并不保证有序, 官方说法是:为了 runtime 能够不断对 map 进行优化, 不能保证 map 遍历的结果是有序的。

func rMap(m map[string]int) {
   for k, v := range m {
      fmt.Println(k, ":", v)
   }
}

map 并不是并发安全, 多个 goroutine 并发写 map 会导致 panic。主要还是出于性能的考虑, 如果将 map 设计为并发安全的, 将大大损失程序的性能。如果是并发场景可以使用 sync.Map

func wMap(m map[string]int) {
   for _, v := range []string{"4", "5", "6"} {
      m[v] = 9
   }
}

// BenchmarkWMap-12     fatal error: concurrent map writes
func BenchmarkWMap(b *testing.B) {
   m := make(map[string]int)
   wg := sync.WaitGroup{}
   for i := 0; i < b.N; i++ {
      wg.Add(1)
      go func() {
         defer func() {
            wg.Done()
         }()
         wMap(m)
      }()
   }
   wg.Wait()
}

实现

map 本质上就是一个 hash table。将 key 使用一个 hash 函数映射到指定的位置, 如果发生 hash 冲突, 通常使用开放寻址法、拉链法解决。在 go 中使用拉链法解决 hash 冲突。能力有限, 不做源码解读了, 这里抛一些点。

聊一聊 map

  • 插入的时候会先通过 hash(key) 确定 key 所属于的 bucket, 一个 bucket 默认存储 8 个键值对, 当插入溢出时会使用链表追加
  • 当负载因子过高「数据元素过多, 防止查找复杂度劣化到 o(n)」或过低「频繁的插入删除导致 bucket 溢出链表过长」时都会触发 map 的扩容操作「翻倍扩容、等容量扩容」. 为了防止一次性扩容导致大量的数据搬迁, go 使用渐进式扩容, 只有在发生插入和删除操作的时候搬迁两个 bucket.

总结

  • 在使用 map 的时候, 如果明确知道所需要存储的数据量, 可以指定容量创建 map, 减少扩容操作。
  • map 在底层实现的时候, 本质上还是通过数组+链表存储数据, 极端情况下性能会劣化到 o(n). 在 go 中, 使用分桶、渐进扩容的方式来保证 map 有尽可能好的性能。
转载自:https://juejin.cn/post/7211062963493486650
评论
请登录