Go map

319 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情 Go 语言中,通过哈希查找表实现 map,用链表法解决哈希冲突。map本身是无序的

  • 通过 key 的哈希值将 key 散落到不同的桶中,每个桶中有 8 个 cell。哈希值的低位决定桶序号,高位标识同一个桶中的不同 key。
  • 当向桶中添加了很多 key,造成元素过多,或者溢出桶太多,就会触发扩容。扩容分为等量扩容和 2 倍容量扩容。扩容后,原来一个 bucket 中的 key 一分为二,会被重新分配到两个桶中。
  • 扩容过程是渐进的,主要是防止一次扩容需要搬迁的 key 数量过多,引发性能问题。触发扩容的时机是增加了新元素,bucket 搬迁的时机则发生在赋值、删除期间,每次最多搬迁两个 bucket。
  • map的声明的时候默认值是nil ,此时进行取值,返回的是对应类型的零值,不存在返回也是零值

  • 向未初始化的map赋值引起 panic: assign to entry in nil map

  • key一定要是可比较的类型(可以理解为支持==的操作):如果是非法的key类型,会报错

    • 不可比较:slice、map、func
    • 可以比较::bool,num,string,pointer,channel,interface,包含前文类型的array和struct
  • 底层数据结构是hmap,是由若干个机构为bmap的bucket组成的数组,每个bucket可以存放若干个元素(通常是8个),那么每个key会根据hash算法归到同一个bucket中,当一个bucket中的元素超过8个的时候,hmap会使用extra中的overflow来扩展存储key

  • img

  • gc:delete是不会真正的把map释放的,所以要回收map还是需要设为nil

基本操作

  • 查找:

    1. 根据key值算出哈希值
    2. 取哈希值低位与hmap.B取模确定bucket位置
    3. 取哈希值高位在tophash数组中查询
    4. 如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较
    5. 当前bucket没有找到,则继续从下个overflow的bucket中查找。
    6. 如果当前处于搬迁过程,则优先从oldbuckets查找

    注:如果查找不到,也不会返回空值,而是返回相应类型的0值。

  • 插入:

    1. 根据key值算出哈希值
    2. 取哈希值低位与hmap.B取模确定bucket位置
    3. 查找该key是否已经存在,如果存在则直接更新值
    4. 如果没找到将key,将key插入

细节问题

  • 可以边遍历边删除吗?
    • map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。 一般而言,这可以通过读写锁来解决这个问题。
      • 读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。
      • 另外,sync.Map 是线程安全的 map,也可以使用。
  • key 可以是 float 型吗?
    • 从语法上看,是可以的。Go 语言中只要是可比较的类型都可以作为 key。支持== 和 != 操作符即可。如果是结构体,则需要它们的字段值都相等,才被认为是相同的 key。任何类型都可以作为 value,包括 map 类型。
    • float 型可以作为 key,但是由于精度的问题,会导致一些诡异的问题,慎用之。

sync.Map

sync.Map的核心数据结构:

type Map struct {
	mu Mutex // 加锁作用。保护后文的dirty字段
	read atomic.Value // 存读的数据,readOnly的数据结构
	dirty map[interface{}]*entry // 包含最新写入的数据。当misses计数达到一定值,将其赋值给read。
	misses int // 计数作用。每次从read中读失败,则计数+1。
}
  • 优点:是官方出的;通过读写分离,降低锁时间来提高效率;
  • 缺点:不适用于大量写的场景,这样会导致read map读不到数据而进一步加锁读取,同时dirty map也会一直晋升为read map,整体性能较差。
  • 适用场景:大量读,少量写

Map,sync.Map,读写锁的适用场景

实现方式原理适用场景
map+Mutex通过Mutex互斥锁来实现多个goroutine对map的串行化访问读写都需要通过Mutex加锁和释放锁,适用于读写比接近的场景
map+RWMutex通过RWMutex来实现对map的读写进行读写锁分离加锁,从而实现读的并发性能提高同Mutex相比适用于读多写少的场景
sync.Map底层通分离读写map和原子指令来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化读多修改少,元素增加删除频率不高的情况,在大多数情况下替代上述两种实现

参考文章

  1. 由浅入深聊聊Golang的map
  2. Golang基础教程——map使用篇
  3. 由浅入深聊聊Golang的sync.Map