golang基础篇(三)【map应用与原理解析】

基础使用

定义:拥有键值对元素的无序集合,键具有唯一性,类型是map[key]value,其中键值都具有相同的数据类型
特点:高性能,低复杂度,无序

初始化
  1. 默认声明
  2. 通过内置函数make初始化
  3. 通过map字面量初始化
func main() {
	// 默认声明
	var map1 map[string]string // 此时map1是零值nil
	//向未初始化的map赋值引起宕机: assign to entry in nil map
	// map1["java"] = "大数据"

	// make初始化
	map2 := make(map[string]string)
	map2["golang"] = "web,容器"
	map2["python"] = "人工智能"

	// 字面量初始化
	map3 := map[string]string{
		"golang": "容器",
		"python": "人工智能",
	}

	// map是安全的,即使map没有引用,或者key不存在,也不会报错
	fmt.Printf(" java 适合做什么:%v \n", map1["java"])
	fmt.Printf(" golang 适合做什么:%v \n", map2["golang"])
	fmt.Printf(" python 适合做什么:%v \n", map3["python"])
}

out

 java 适合做什么: 
 golang 适合做什么:web,容器 
 python 适合做什么:人工智能 

map增删改查

根据上面初始化的方法,接下来我们对map进行常规操作
注意:map是无序的,如果想要顺序输出,需要对map的键手动排序,再下面最后的demo中会有示例


func main() {

	/*
		1. 先初始化一个map
		2. 循环加入10个元素
		3. 删除指定的元素
		4. 对指定key的元素值进行重置
		5. 读取操作后的map和遍历map
	*/
	map1 := make(map[int]string)
	for i := 1; i <= 10; i++ {
		map1[i] = strconv.Itoa(i)
	}
	fmt.Printf("原始map:%v \n", map1)
	// 删除key 5的元素
	delete(map1, 5)
	delete(map1, 20) // 删除一个不存在的key,不会报错,map不进行操作
	fmt.Printf("删除后的map:%v \n", map1)
	// 重置20,40 的值
	map1[2] = "20"
	map1[4] = "40"

	//获取值
	fmt.Printf(" map key 5 ,value is %v \n", map1[2])
	// range 遍历map,map的结果是无序的
	for key, value := range map1 {
		fmt.Printf(" map key is %v  ,value is %v \n", key, value)
	}

	// 手动排序
	keys := make([]int, 0, len(map1))
	// 把key单独抽取出来,放在切片中
	for key := range map1 {
		keys = append(keys, key)
	}
	// 进行数组的排序
	sort.Ints(keys)
	// 遍历数组就是有序的了
	for _, key := range keys {
		fmt.Printf(" map key is %v  ,value is %v \n", key, map1[key])
	}
}

out

原始mapmap[1:1 2:2 3:3 4:4 5:5 6:6 7:7 8:8 9:9 10:10] 

删除后的mapmap[1:1 2:2 3:3 4:4 6:6 7:7 8:8 9:9 10:10] 

 map key 5 ,value is 20 
 
 map key is 7  ,value is 7 
 map key is 9  ,value is 9 
 map key is 10  ,value is 10 
 map key is 3  ,value is 3 
 map key is 2  ,value is 20 
 map key is 4  ,value is 40 
 map key is 6  ,value is 6 
 map key is 8  ,value is 8 
 map key is 1  ,value is 1 

 map key is 1  ,value is 1 
 map key is 2  ,value is 20 
 map key is 3  ,value is 3 
 map key is 4  ,value is 40 
 map key is 6  ,value is 6 
 map key is 7  ,value is 7 
 map key is 8  ,value is 8 
 map key is 9  ,value is 9 
 map key is 10  ,value is 10 

map的两种取值逻辑

  1. 直接取value,忽略bool判断
  2. 获取bool判断和value

对于两套取值方式,使用情况下区别不大,但是对于底层实现还是有区分的,取值的不同对应的不同原理,双参数返回适用于if流程判断

func main() {

	map1 := make(map[int]string)
	for i := 1; i <= 10; i++ {
		map1[i] = strconv.Itoa(i)
	}
	// fmt.Printf(" 原始map:%v \n", map1)

	// 1. 直接获取value
	value1 := map1[2]
	fmt.Printf(" value is  %v \n", value1)

	// 2. 获取判断bool,和value
	value2, ok := map1[2]
	fmt.Printf(" is has :%v ,value is  %v \n", ok, value2)

	// 双参数返回适用于if流程判断,以下示例
	if value2, ok := map1[2]; ok {
		fmt.Printf(" map1 has  %v \n", value2)
	}
}

out

 value is  2 
 is has :true ,value is  2 
 map1 has  2 
并发中的map
func main() {
	map1 := make(map[int]int)
	go func() {
		// 死循环,类似while true
		// 不停的赋值
		for {
			map1[0] = 1
		}
	}()
	go func() {
		// 不停的读取值
		for {
			_ = map1[1]
		}
	}()

	// 控制程序不断调,其实这里可以用通道控制,
	// 通道的基础,后面会涉及
	for {
	}
}

out

并发读写后报错了
fatal error: concurrent map read and map write

sync.Map
golang在1.9版本中加入了并发安全的map,和其他语言的区别在于,并发安全的map并不是语言原生支持的,需要借助内置包sync使用

sync.Map 特点

  1. 存储方式的变更,通过函数的形式
  2. range遍历时, 需要用回调函数来配合使用,用bool类型标记是否需要继续操作
缺点:
1.  没有提供获取数量的方法,需要在遍历时自行获取
2.  性能不如map
func main() {

	var scene sync.Map
	go func() {
		// 死循环,类似while true
		// 不停的赋值
		for {
			scene.Store("java", "大数据")
		}
	}()
	go func() {
		// 不停的读取值
		for {
			fmt.Println(scene.Load("java"))
		}
	}()

	// 控制程序不断调,其实这里可以用通道控制,
	// 通道的基础,后面会涉及
	for {
	}
}

out

大数据 true
大数据 true
大数据 true
大数据 true
...

sync.Map的遍历


func main() {
	var scene sync.Map
	// 将键值对保存到sync.Map
	scene.Store("java", "大数据")
	scene.Store("golang", "容器")
	scene.Store("python", "人工智能")
	// 从sync.Map中根据键取值
	fmt.Println(scene.Load("java"))
	// 根据键删除对应的键值对
	scene.Delete("golang")
	// 遍历所有sync.Map中的键值对
	scene.Range(func(k, v interface{}) bool {
		fmt.Println("item :", k, v)
		return true
	})
}

out

大数据 true
item : java 大数据
item : python 人工智能

原理解析(runtime)

存储属性
// A header for a Go map.
type hmap struct {
    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)

    extra *mapextra // optional fields
}
  1. count :Map中元素的数量
  2. flags : 表示当前Map的状态
  3. B:map的bucket数组长度的对数
  4. noverflow 为Map中溢出桶的数量.当溢出桶太多的时候,Map会进行same-size map growth.其实质是为了避免溢出桶过大导致的内存泄露问题
  5. hash0 代表生成hash的随机数种子
  6. buckets 指向了当前Map对应的桶的指针
  7. oldbuckets 是在Map进行扩容的时候存储旧桶的,当所有的旧桶中的数据都已经转移到了新桶,则清空
  8. nevacuate 在扩容的时候使用。用于标记当前旧桶中小于nevacuate的桶都已经转移到了新桶
  9. extra存储Map中的溢出桶

上述中buckets是一个指针,指向实际存储的bucket数组的首个字段: 即一个固定长度为8的数组。此字段顺序存储key的哈希值的前8位.

type bmap struct {
    tophash [bucketCnt]uint8
}

运行时仅仅通过指针操作即可找到特定位置的元素,所以以上结构并不是golang在运行时,在编译阶段编译器会动态的创建一个新的结构

type bmap struct {
    topbits  [8]uint8
    keys     [8]keytype
    values   [8]valuetype
    pad      uintptr
    overflow uintptr
}

在golang中,map的存储并不是key/value形式的存储,而key和value分开存储是为了更好的在存储过程中进行空间的压缩对齐,如下图(参考gocn)

在这里插入图片描述

溢出桶

当指定桶中的数据超过了8个,并不会直接就新开辟一个新桶,而是会将数据放置到溢出桶中,每个桶的最后还存储了overflow 即溢出桶的指针

如果一开始初始化map的数量比较大。则map提前创建好一些溢出桶存储在extra *mapextra 字段
正常操作,数据是很少会跑到溢出桶里面去的,在查找操作时,tophash数组没有制定的可以,还会去遍历溢出桶的数组

负载因子
负载因子 = 哈希表中元素数量 / 桶的数量
  • 负载因子越大,说明每个桶的元素越多,效率越低
  • golang的负载因子为6.5 ,超过临界值,则map会以2倍大小进行扩容
  • 旧桶的数据会首先存到oldbuckets中,后分散的转移到新桶中,数据转移完毕后,旧桶会被清除
Map重建
  • 当Map超过了负载因子大小
  • 当溢出桶的数量过多

负载因子过大重建

针对上述负载因子,大于6.5后,map重建扩容,扩容后负载因子会降到额定值以下
数据也会从oldbuckets转移到buckets,此过程称为双倍重建

溢出桶过多重建

溢出桶重建不是太好理解,正常来说,数据是很少会跑到溢出桶里面去的,但是在频繁操作插入和删除的情况下,还没有触发负载过大重建,元素已经被删掉了,会导致分配的buckets很多,同时也会创建大量的overflow bucket,这种情况下key 会很分散,查找插入效率非常低,但是此时的负载因子却没达到临界值(每个桶中的元素比较少)

针对这种特殊情况,golang有了第二个重建机制,新开一个bucket,将老 bucket 中的元素移动到新 bucket并重新排列key,原本overflow bucket 中的 key 也移动到新的bucket中来,减少溢出桶的数量,节省内存空间,提高查询和插入效率



Map-delete

与赋值操作类似,会找先到指定的桶,如果存在指定的key,那么就释放掉key与value引用的内存。同时tophash中指定位置会存储emptyOne,代表当前位置是空的,同时在删除操作,会探测到是否当前要删除的元素之后都是空的。如果是,tophash会存储为emptyRest. 这样做的好处是在做查找操作时,遇到emptyRest 可以直接退出,因为后面的元素都是空的


原理解析,参考GoCN

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值