1.初始化:
1.1字面量初始化:
func main() { m := map[string]string{ "hello": "world", "hello2": "world2", } for k, v := range m { fmt.Printf("%s-%s\n", k, v) } }1.2内置函数make初始化:
func main() { m2 := make(map[string]int, 10) m2["hello"] = 1 m2["hello2"] = 2 for k, v := range m2 { fmt.Printf("%s-%d\n", k, v) } }使用make内置函数初始化map时可以指定容量(也可以不指定).指定容量可以有效的减少分配内存次数.有利于提升性能.
1.3map的增删改查:
func main() { m2 := make(map[string]int, 10) for k, v := range m2 { fmt.Printf("%s-%d\n", k, v) } //添加. m2["hello"] = 1 m2["hello2"] = 2 m2["add"] = 3 //修改. m2["hello"] = 001 //删除 delete(m2, "hello2") //查询 v, exist := m2["add"] if exist { fmt.Println(v) } }注:
修改操作中.如果键不存在.则map会创建一个新的键值对并存储.等同于添加元素.
删除元素使用内置函数delete()完成.没有返回值.在map为nil或指定键值不存在的话.也不会报错.相当于空操作.
查询操作中.最多可以给两个变量赋值.第一个为值.第二个为bool类型变量.用于表示是否存在指定键.如果不存在.对应的值为相应类型的零值.如果指定一个变量.那么该变量仅表示该键对应的值.如果键不存在.该值同样为相应类型的零值.
内置函数len()可以查询map的长度.该长度为map中键值对的数量.
2.危险操作:
2.1并发读写:
map操作不是原子的.这意味这多协程操作map时有可能产生读写冲突.读写冲突会发生panic从而导致程序退出.
2.2空map:
未初始化的map值为nil.在向值为nil的map添加元素的时候会发生panic.
值为nil的map和长度为空的map长度一致.
func main() { var m1 map[string]int m := make(map[string]int) if len(m1) == len(m) { fmt.Println("长度一致") } }3.实现原理:
3.1数据结构:
Go语言的map底层使用的是hash实现.一个hash表里可以有多个bucket.而每个bucket保存了map中的一个或一组键值对.
源码位置:runtime/map_noswiss.go:hmap(go版本1.24.0)
type hmap struct { // Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go. // Make sure this stays in sync with the compiler's definition. count int // # live cells == size of map. Must be first (used by len() builtin) flags uint8 B uint8 // log_2 of # of buckets (can hold up to loadFactor * 2^B items) noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details hash0 uint32 // hash seed buckets unsafe.Pointer // array of 2^B Buckets. may be nil if count==0. oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing nevacuate uintptr // progress counter for evacuation (buckets less than this have been evacuated) clearSeq uint64 extra *mapextra // optional fields }count 当前保存元素的个数.
B bucket数组大小.
buckets bucket数组.数组长度为2的b次方.
oldbuckets 旧bucket数组.用于扩容.
拥有4个bucket的map.
3.2bucket结构:
type bmap struct { // tophash generally contains the top byte of the hash value // for each key in this bucket. If tophash[0] < minTopHash, // tophash[0] is a bucket evacuation state instead. tophash [abi.OldMapBucketCount]uint8 // Followed by bucketCnt keys and then bucketCnt elems. // NOTE: packing all the keys together and then all the elems together makes the // code a bit more complicated than alternating key/elem/key/elem/... but it allows // us to eliminate padding which would be needed for, e.g., map[int64]int8. // Followed by an overflow pointer. }tophash 存储hash值得高八位.从下面源码可以看出.值为8.
const ( // Maximum number of key/elem pairs a bucket can hold. OldMapBucketCountBits = 3 // log2 of number of elements in a bucket. OldMapBucketCount = 1 << OldMapBucketCountBits // Maximum key or elem size to keep inline (instead of mallocing per element). // Must fit in a uint8. // Note: fast map functions cannot handle big elems (bigger than MapMaxElemBytes). OldMapMaxKeyBytes = 128 OldMapMaxElemBytes = 128 // Must fit in a uint8. )tophash 是一个长度为8的整型数组.hash值相同的键.(准确的说是hash值低八位相同的键)存入当前bucket时会将hash值的高八位存储在该数组中.方便后续匹配.
3.3hash冲突:
当有两个或两个以上的key被hash到通一个bucket时.我们称这些键发生了hash冲突.Go使用地址法来解决hash冲突.
一个桶可以放8个键值对.如果超过了8个会重新在创建一个bucket.用类似链表的方式连接起来.
注:hash冲突造成了存取效率低下.
3.4负载因子:
负载因子用于衡量一个hash表冲突情况.公式如下:
负载因子=键数量/bucket数量
示例:
一个bucket数量为4.包含4个key-value的hash表.负载因子为1.
总结:
负载因子过小.说明空间利用率低.
负载因子过小.可能是预分配的空间过大.也可能是大部分元素被删除造成的.
负载因子过大.说明冲突严重.存取效率低.
当hash表中负载因子过大.需要申请更多地bucket.并对所有键值对重新组织.使其均匀的分配到bucket中.这个过程称为rehash.
3.5扩容:
降低负载因子常用的手段就是扩容.为了保证访问效率.当新元素加入的时候会检查是否需要扩容.扩容就是通过空间来换取时间的手段.
3.5.1扩容条件:
负载因子大于6.5.即平均每个桶存储的键值对达到6.5个以上.
overflow的数量达到2的min(15,b).
3.5.2增量扩容:
当负载因子过大时.新建一个bucket数组.新的bucket数组是原来的2倍.然后旧的bucket数组搬迁到新的bucket数组.如果map中存储了数以亿计的键值对.一次性搬迁会造成较大的延时.Go采用了逐步搬迁的策略.即每次访问map的时候都会进行搬迁.每次搬迁两个桶.
负载因子为7/1=7.
扩容的时候先让oldbuckets指向原bucket数组.然后申请新的数组(长度为原来的倍).并将数组指针保存到bucket.等后续迁移完了安全释放oldbuckets.迁移完成如下.
3.5.3等量扩容:
等量扩容并不是扩大容量.bucket数量不变.重新做一遍类似增量扩容的搬迁操作.把松散的键值对重新排列一次.使bucket使用效率更高.保证存取速度.
4增删改查:
无论元素的添加还是查询操作.都需要根据键的hash值确定一个bucket.并查询该bucket中是否存在指定的键.
查询操作:
查到指定的键后获取值就返回.否则返回对应类型的空值.
添加操作:
查到指定的键意味着当前操作实际上是更新操作.否则在bucket中查找一个空余位置插入.
4.1查找过程:
1).根据key值计算hash值.
2).取hash值低位与hmap.B取模来确定bucket的位置.
3).取hash值高位.在tophash数组中查询.
4).如果tophash[i]中存储的hash值与当前key的hash值相等.则获取tophash[i]的key值进行比较.
5).如果当前bucket中没有找到.则依次从溢出的bucket中查找.
6).如果还是找不到不是返回nil.而是返回对应类型的零值.
4.2添加过程:
1).根据key值计算出hash值.
2).取hash值低位与hmp.B取模来确定bucket位置.
3).查找key是否存在.如果存在则直接更新值.
4).如果key不存在,则从该bucket中寻找空余位置并插入.
注:如果当前map处于搬迁过程中.则元素会直接添加到新的bucket数组中.查找过程仍从就数组开始.
4.3删除过程:
删除元素实际上是先查找元素.如果元素存在从相应的bucket中清除.如果不存在则什么也不做.
眼里倒映着转身.
如果大家喜欢我的分享的话.可以关注我的微信公众号
念何架构之路