一.map的基本用法
1.1 map的初始化
map的初始化方法分为字面量初始化和使用make初始化
func main() {
//字面量初始化
test := map[string]int{
"测试1": 1,
"测试2": 2,
}
for key, value := range test {
fmt.Println(key, value)
}
//使用make初始化map
makeMap := make(map[string]int)
makeMap["测试3"] = 3
makeMap["测试4"] = 4
for key, value := range makeMap {
fmt.Println(key, value)
}
}
使用内置函数make声明map集合时,可以通过make(map[string]int , cap),指定容量,可以减少内存分配次数.
1.2 map集合的增删改查
func main() {
//字面量初始化
test := map[string]int{
"测试1": 1,
"测试2": 2,
}
test["测试1"] = 11 //改
test["测试3"] = 3 //增
//查询
value, ok := test["测试1"]
if ok {
//如果存在
fmt.Println(value)
}
//删除
delete(test, "测试1") //测试1被删除
delete(test, "测试删除") //删除的键不存在,不会报错,相当于空操作
for key, value := range test {
fmt.Println(key, value)
}
}
注意修改的操作,如果key不存在,就相当于增加操作. 查询时,会给两个变量赋值,第一个是对应键的值,如果该键不存在,则为对应类型的零值.第二个值是一个bool类型的变量,判断该键是否存在.
可以使用len(),去查询map的长度.即map的键值对的数量.
1.3 补充
map集合也可以通过 var test map[string]int 声明 , 但此时map的值为nil , 添加元素会触发panic.
func main() {
var test map[string]int
test["11"] = 1
fmt.Println(test)
}
panic: assignment to entry in nil map
二.map实现原理
Go语言的map使用Hash表作为底层实现,一个哈希表中可以有多个桶(bucket),每一个哈希桶可以保存一个或一组键值对.
2.1 map的数据结构
//map的核心结构体 runtime/map.go:hamp
type hmap struct{
count int //当前保存的元素的个数
B uint8 //bucket 当前哈希表中buckets数量
buckets unsafe.Pointer //bucket数组,数组的长度为2^B
oldbuckets unsafe.Pointer //旧bucket数据 , 用于扩容
....
}
2.2 bucket的数据结构
type bmap struct{
tophash [8]uint8 //存储Hash值得高8位
data []byte //key value 数据 key/key/key.../value/value/value... 节省字节对齐浪费的内存空间
overflow *bmap //溢出bucket的地址 指针指向一个新的桶
}
每个bucket是可以存储8个键值对的.
topash是一个长度为8的整型数组,hash低位相同的键存入当前bucket时会将Hash值高位存储在该数组中.
bucket的数据结构中并没有显式地在结构体中声明,运行时在访问bucket时,直接通过指针偏移访问这些虚拟成员.
2.3 Hash冲突
当有两个或两个以上的数量的键被"Hash"到了同一个bucket时,即发生了Hash冲突.Go采用的是链地址解决的哈希冲突,当一个bucket存放超过8个键值对时,会创建一个新的bucket,用类似于链表的方式将两个bucket链接起来.
2.3 负载因子
负载因子是一个衡量Hash表的冲突情况 , 公式为: 负载因子 = 键数量 /bucket数量
例如:一个bucket数量为4,包含4个键值对的Hash表来说,负载因子为1.
当负载因子过大或过小时都不是理想情况:
1.当负载因子过小,说明空间利用率低
2.当负载因子过大, 说明冲突严重,存取效率低.
负载因子过小,可能是因为预分配的空间过大,或者是大部分元素被删除造成的.随着元素不断添加到map中,负载因子会逐渐升高.
当Hash表中负载因子过大,需要不断申请bucket,并对所有的键值重新组织,使其均匀分布到这些bucket中,这个过程被称为rehash.
每个Hash表的实现对负载因子的容忍情况不同, Go语言的bucket可以存放8个键值对,,在负载因子到大6.5时,才会触发rehash.而Redis中bucket只能存放一个键值对,因此只要负载因子大于1就会触发rehash.
2.4 扩容
2.4.1 扩容的条件
1.负载因子大于6.5,即平均每一个桶的键值对数量达到了6.5个以上
2.overflow的数量达到2^min(15,B)时.
扩容是降低负载因子的常用手段,为提高访问效率,当新元素将要添加进map时,都会检查是否需要扩容.
2.4.2 增量扩容
当负载因子过大,就会新建一个bucket数组,新的bucket数组的长度是原来的2倍,然后旧的bucket数组中的元素,搬迁到新的bucket中.而Go为了防止当数据量过大时一次搬迁导致的延时,Go采用的是逐步搬迁的策略,即每次访问map时都会触发一次搬迁,每次搬迁触2个键值对.
2.5.2 等量扩容
等量扩容并不是扩大容量,二至bucket数量不变,重新做一遍类似增量扩容的搬迁操作.把松散的键值对重新排列,提高bucket的使用率.当bucket过多,而键值对少的情况下会发生等量扩容.
2.5 增删改查
无论是元素的添加还是查询,都需要先根据键的Hash值确定一个bucket,并且查询该bucket中是否存在该键.对于添加操作而言,如果查询到键存在,则是修改操作.
查找过程:
1.根据key计算出Hash值;
2.取Hash值低位与hamp.B取模确定bucket的位置;
3.取Hash值高位,再topash数组中查询.
4,如果topash[i]中存储的Hash值与当前key的Hash值相等,则获取topash[i]中的key值进行比较.
5.当前bucket中没有找到,则依次从溢出的bucket中查找.
如果当前map处于搬迁过程中,那么查找时优先从oldbuckets中查找.