本文将从多个方面对go map源码进行详细的阐述,包括map的原理、构造方法、基本操作、扩容机制等,让读者对map有更深入的理解。
一、map的原理
在go中,map是一种无序的键值对集合。其底层实现是哈希表。基于哈希表的实现能够快速地根据键访问值,在提供近乎常数时间的复杂度的同时,保证添加、删除、查找、改变某个键值等操作的效率。
哈希表的基本思想就是将键映射到数组索引的计算过程,这个计算过程称为哈希函数。具体的实现是:先根据键算出哈希值,然后使用哈希值对数组的长度取模得到一个索引值,最后将该键值对插到该索引处。
然而,在实际使用中,哈希表会存在哈希冲突的问题,即不同键映射到同一个索引处。此时,可以将键值对插入数组索引处的位置表示成一个链表/树,称之为桶(bucket)。Go的map底层使用的是散列表(哈希表)和链表(桶)混合实现,初始的桶可称之为BUCKET数组 (map结构体所包含的hmap结构体中), 为了尽量避免哈希冲突,map内部对BUCKET数组循环使用……
二、map的构造方法
go标准库的map构造方法如下:
func make(map[int]int, 100) // 创建一个可存100个键值对的map,键和值都是int类型。
make(map[k键类型]v值类型, initSize初始长度或者分配的内存大小)
关于内存大小,make函数通过内置函数new申请一个指定类型的对象,并返回指向该对象的指针。 我们可以使用GODEBUG="gctrace=1"环境测试内存自动管理gc。
三、map的基本操作
Go标准库提供了map的以下基本操作:
1. 判断key是否在map中
_, ok := m[key]
通过_, ok := m[key],我们不仅可以判断一个值是否存在,而且可以直接访问到值,完成操作。
2. 获取值
value, ok := m[key]
如果key存在于map中,value将是相应的值且ok为true;否则value将是该值类型的零值且ok为false。
3. 修改元素
m[key] = value
如果key存在于map中,它的值将被value替换。否则,它将插入一个新元素。
4. 删除元素
delete(m, key)
从map中删除元素key。如果key在map中不存在,则不执行任何操作。
四、map的扩容机制
当map中元素个数大于hmap结构体中的B指定pts,即桶数量时,会触发map扩容,也就是重新分配BUCKET数组,具体操作是:
1. 申请新BUCKET数组
oldbuckets := hmap.buckets newbuckets := make([]*bmap, nextBucketCnt, ptrSize )// ptrSize是指针长度,下一个扩容桶的长度为:BUCKET存储的元素的个数*2 以上nextBucketCnt计算方式为: if h.sameSizeGrow()<0 { // sameSizeGrow是判断是否连续扩容,连续扩容的情况下桶个数每次翻倍增长! nextBucketCnt = uintptr(h.BUCKETCOUNT<<1) + 1} else { nextBucketCnt = h.BUCKETCOUNT + uintptr(notSameSizeGrowStep*h.BUCKETCOUNT>>Q)}
2. 寻找新的hash值
遍历BUCKET数组,将元素放入新的BUCKET数组中。
3. 将老的BUCKET数组中的元素对象清空
for i := 0; i < len(oldbuckets); i++ { oldbucket := oldbuckets[i] if oldbucket != nil { for j := 0; j < len(oldbucket)); j++ { e := &oldbucket[j] if e.key != nil { // 清空 e.key = nil e.value = nil } } } }
至此,map扩容操作就完成了。需要注意的是,go语言中map的元素类型只要内存地址不变,插入元素的键和值为非指针类型的做write操作将可能导致:值由于出现哈希冲突被重新分配到新的桶中了。
五、总结
本文主要对go map源码进行了分析,包括map的原理、构造方法、基本操作、扩容机制等。通过对map的深入剖析,更好地了解了go语言的map数据结构,希望能够对读者和开发者有所帮助。