map内存模型
// A header for a Go map.
type hmap struct {
// 元素个数,调用 len(map) 时,直接返回此值
count int
flags uint8
// buckets 的对数 log_2
B uint8
// overflow 的 bucket 近似数
noverflow uint16
// 计算 key 的哈希的时候会传入哈希函数
hash0 uint32
// 指向 buckets 数组,大小为 2^B
// 如果元素个数为0,就为 nil
buckets unsafe.Pointer
// 扩容的时候,buckets 长度会是 oldbuckets 的两倍
oldbuckets unsafe.Pointer
// 指示扩容进度,小于此地址的 buckets 迁移完成
nevacuate uintptr
extra *mapextra // optional fields
}
B是buckets数组的长度的对数,buckets数组的长度就是2^B。
bmap就是桶,桶里面会最多装8个key。在桶内,根据key计算出来的hash值的高8位来决定key到底落入桶内的哪个位置(一个桶内最多有8个位置)。
bmap是存放k-v的地方。
每个bucket设计成最多只能放8个key-value对,如果有第9个key-value落入当前的 bucket,通过overflow指针连接新的bucket。
key定位过程
key经过哈希计算后得到哈希值,共64个bit位,计算它到底要落在哪个桶时,只会用到最后B个bit位。如果 B = 5,那么桶的数量,也就是 buckets 数组的长度是 2^5 = 32。
用哈希值的高8位,找到此key在bucket中的位置。先找到对应的桶,再去遍历bucket 中的key。
扩容
装载因子是key总数/桶的总数
loadFactor := count / (2^B)
插入新key时,进行条件检测,符合下面这2个条件,就会触发扩容。
装载因子超过阈值6.5(每个bucket有8个空位,在没有溢出,且所有的桶都装满了的情况下,装载因子算出来的结果是8。当装载因子超过6.5时,表明很多bucket都快要装满了,查找效率和插入效率都变低了。)——2倍扩容,新桶位置下标要么保持不变,要么加上2^B
overflow的bucket 数量过多:当桶总数小于2^15时,如果overflow的bucket数量超过桶总数;当桶总数大于等于2^15,如果overflow的bucket数量超过2^15。(第一种情况的补充,在装载因子比较小的情况下,查找和插入效率也很低,表面现象就是map里元素总数少,但是bucket数量多)——等量扩容
可以边遍历边删除吗
map不是一个线程安全的,同时读写一个map被检测到,会直接panic。可以通过读写锁来解决:sync.RWMutex。
key可以是float32或者float64型吗
从语法上看,是可以的。只要是可比较的类型都可以作为 key。除了slice,map,functions 这几种类型,其他类型都是OK的。任何类型都可以作为value,包括map类型。
当用float64作为key的时候,先转成unit64类型,再插入key中。2.4和 2.4000000000000000000000001经过math.Float64bits函数转换后的结果是一样的。二者在 map看来,就是同一个key了。
所以,虽然float32或者float64型可以作为 key,但是精度会带来其他问题。
sync.map很少使用的原因
在以下两个场景中使用sync.Map,会比使用map+RWMutex的方式,性能好得多:
只会增长的缓存系统中,一个key只写入一次而被读很多次;
多个goroutine为不相交的键集读、写和重写键值对。
总结
通过哈希查找表实现map,用链表法解决哈希冲突。
当向桶中添加了很多 key,造成元素过多,或者溢出桶太多,就会触发扩容。扩容分为等量扩容和2倍容量扩容。扩容后,原来一个bucket中的key一分为二,会被重新分配到两个桶中。
扩容过程是渐进的,主要是防止一次扩容需要搬迁的key数量过多,引发性能问题。触发扩容的时机是增加了新元素,bucket搬迁的时机则发生在赋值、删除期间,每次最多搬迁两个 bucket。