Go语言map的使用

一.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中查找.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值