持续创作,加速成长!这是我参与「掘金日新计划 · 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。
-
-
gc:delete是不会真正的把map释放的,所以要回收map还是需要设为nil
基本操作
-
查找:
- 根据key值算出哈希值
- 取哈希值低位与hmap.B取模确定bucket位置
- 取哈希值高位在tophash数组中查询
- 如果tophash[i]中存储值也哈希值相等,则去找到该bucket中的key值进行比较
- 当前bucket没有找到,则继续从下个overflow的bucket中查找。
- 如果当前处于搬迁过程,则优先从oldbuckets查找
注:如果查找不到,也不会返回空值,而是返回相应类型的0值。
-
插入:
- 根据key值算出哈希值
- 取哈希值低位与hmap.B取模确定bucket位置
- 查找该key是否已经存在,如果存在则直接更新值
- 如果没找到将key,将key插入
细节问题
- 可以边遍历边删除吗?
- map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。 一般而言,这可以通过读写锁来解决这个问题。
- 读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。
- 另外,sync.Map 是线程安全的 map,也可以使用。
- map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。 一般而言,这可以通过读写锁来解决这个问题。
- 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和原子指令来实现读的近似无锁,并通过延迟更新的方式来保证读的无锁化 | 读多修改少,元素增加删除频率不高的情况,在大多数情况下替代上述两种实现 |