Golang map分析

Go语言中的map是引用类型,必须初始化才能使用

Golang采用了HashTable的实现,解决冲突采用的是链地址法。也就是说,使用数组+链表来实现map

map实际使用的结构体hmap

在运行期间,runtime.bmap 结构体其实不止包含 tophash 字段

当map存入一个值时,会触发mapassign 函数的调用

//计算key的hash值
hash := alg.hash(key, uintptr(h.hash0))
//计算key落入哪个捅号
bucket := hash & bucketMask(h.B)
//拿桶号计算偏移地址 来获取该桶的起始地址
b := (*bmap)(unsafe.Pointer(uintptr(h.buckets) + bucket*uintptr(t.bucketsize)))
//hash的高8位
top := tophash(hash)
// emptyRest      = 0 // 此单元为空,且更高索引的单元也为空
  // emptyOne       = 1 // 此单元为空
  // evacuatedX     = 2 // 用于表示扩容迁移到新桶前半段区间
  // evacuatedY     = 3 // 用于表示扩容迁移到新桶后半段区间
  // evacuatedEmpty = 4 // 用于表示此单元已迁移
  // minTopHash     = 5 // 最小的空桶标记值,小于其则是空桶标志

遍历bmap中的8个坑,找到合适的坑去存key value,如果hash高8位一样 而且key也一样 那就更新value

随着哈希表存储的数据逐渐增多,我们会扩容哈希表或者使用额外的桶存储溢出的数据,不会让单个桶中的数据超过 8 个,不过溢出桶只是临时的解决方案,创建过多的溢出桶最终也会导致哈希的扩容。

  • 当桶的数量小于 2^4 时,由于数据较少、使用溢出桶的可能性较低,会省略创建的过程以减少额外开销;
  • 当桶的数量多于 2^4 时,会额外创建 2^(B−4) 个溢出桶   h.nextOverflow设置为第一个可用的overflow

一:扩容策略

1:map中的键值对数量 > 8 并且 键值对数量/桶数 > 6.5时,会引发翻倍扩容         overLoadFactor

如果B为0 桶的个数为1个   键值对数量为7   7也是大于 6.5的 这个时候没有必要扩容,所以 count > bucketCnt 把这种情况排除了

2:使用了太多的溢出桶(溢出桶太多,会导致map处理速度降低)        tooManyOverflowBuckets

a:B <=15 已使用的溢出桶数量 >= 2^B 时,会引发等量扩容

b:B >=15 已使用的溢出桶数量 >= 2^15时,会引发等量扩容

B最大是15   B&0x00001111 的结果都是B   unint16(1) << (B&15)  其实就是 2^B 

模拟等量扩容的场景

以B=2为例,有4个桶,有24个key的键值对落在了A桶,此时需要3个桶(1个原桶+2个溢出桶),然后把这24个键值对删掉,此时桶里边有2个溢出桶 0个键值对。。。再来24个键值对落在B桶,同样的道理需要2个溢出桶,再删除。。。依次类推,不触发翻倍扩容,触发等量扩容。。。。

通过k获取hash值,hash值的低B位和bucket数组长度取余,定位到在数组中的那个下标,hash值的高八位存储在bucket中的tophash中,用来快速判断key是否存在,key和value的具体值则通过指针运算存储,当一个bucket满时,通过overfolw指针链接到下一个bucket

二:迁移策略

// mapassign 中创建新bucket时检测是否需要扩容
if !h.growing() && //非扩容中
  (overLoadFactor(h.count+1, h.B) || tooManyOverflowBuckets(h.noverflow, h.B)) {
  // 提交扩容,生成新桶,记录旧桶相关。但不开始
  // 具体开始是后续赋值和删除期间渐进进行
  hashGrow(t, h)
}

//mapassign 或 mapdelete中 渐进扩容
bucket := hash & bucketMask(h.B)
if h.growing() {
  growWork(t, h, bucket)
}

// 具体迁移工作执行,每次最多两个桶
func growWork(t *maptype, h *hmap, bucket uintptr) {
  // 迁移对应旧桶
  // 若无迭代器遍历旧桶,可释放对应的overflow桶或k/v
  // 全部迁移完则释放整个旧桶
  evacuate(t, h, bucket&h.oldbucketmask())

  // 如果还有旧桶待迁移,再迁移一个
  if h.growing() {
    evacuate(t, h, h.nevacuate)
  }
}

hashGrow()把桶分配好,并没有真正搬迁。。搬迁过程是在growWork()中进行的  growWork()是由mapassign 和 mapdelete 函数中也就是插入或修改、删除 key 的时候,都会尝试进行搬迁 buckets 的工作。先检查 oldbuckets 是否搬迁完毕,具体来说就是检查 oldbuckets 是否为 nil。

------------------------------------------------------2021--06--08-------------------------------------------------------------

无序的  基于key--value的数据结构,内部使用散列表 hash 实现

map声明 初始化

map[KeyType]ValueType
//KeyType:表示键的类型。
//ValueType:表示键对应的值的类型。
make(map[KeyType]ValueType, [cap])
//map类型的变量默认初始值为nil,需要使用make()函数来分配内存 cap值虽不是必须的但是初始化时应该指定一个合适的值

scoreMap := make(map[string]int, 1)
scoreMap["张三"] = 90
scoreMap["小明"] = 100

//map也支持在声明的时候填充元素   字面量初始化。。。。。
userInfo := map[string]string{
		"username": "沙河小王子",
		"password": "123456",
	}

判断某个键时候存在。。。

value, ok := map[key]
 如果key存在ok为true,v为对应的值;不存在ok为false,v为值类型的零值

map的遍历

scoreMap := make(map[string]int) //cap就没有指定数据
scoreMap["张三"] = 90
scoreMap["小明"] = 100
scoreMap["娜扎"] = 60
for k, v := range scoreMap {
  fmt.Println(k, v)
}
//只遍历key
for k := range scoreMap{
  fmt.Println(k)
}

map 删除

delete(map, key)
delete(scoreMap, "小明")//将小明:100从map中删除  如果删除一个不存在的key 也不会报错
//go doc builtin.delete

map 常见的坑。。。

func main() {
	type Map map[string][]int
	m := make(Map)
	s := []int{1, 2}
	s = append(s, 3)
	fmt.Printf("%p %p %+v\n",s,&s,s)		//0xc00009c000 0xc00008a020 [1 2 3]
	m["sandy"] = s
	s = append(s[:1], s[2:]...)
	fmt.Printf("%p %p %+v\n",s,&s, s)		//0xc00009c000 0xc00008a020 [1 3]
	fmt.Printf("%p %p %+v\n", m["sandy"],&s,m["sandy"]) //0xc00009c000 0xc00008a020 [1 3 3]
}

append前后 由于是裁剪slice  s的值一直都是0xc00009c000 

type Map map[string][]int
m := make(Map)
s := []int{1, 2}
s = append(s, 3)
fmt.Printf("%p %p %+v\n",s,&s,s)		//0xc00009c000 0xc000088020 [1 2 3]
m["sandy"] = s
for i := 0; i < 10; i++ {
  s = append(s,i)
}
fmt.Printf("%p %p %+v\n",s,&s, s)		//0xc0000a0000 0xc000088020 [1 2 3 0 1 2 3 4 5 6 7 8 9]
fmt.Printf("%p %p %+v\n", m["sandy"],&s,m["sandy"]) //0xc00009c000 0xc000088020 [1 2 3]

append前后 slice发生了扩容  s的值变化了0xc00009c000--->0xc0000a0000map保存的依然是0xc00009c000处的值

func test(a *int){
	fmt.Printf("in test a=%p\n",a)
	fmt.Printf("int test address a=%p\n",&a)
	*a = 200
}
func main() {
	a := 100
	fmt.Printf("in main a=%d\n",a)
	fmt.Printf("int main address a=%p\n",&a)
	test(&a)
	fmt.Printf("a=%d\n",a)
}
/*
in main a=100
int main address a=0xc000062090
in test a=0xc000062090
int test address a=0xc00008c020
a=200
*/

 map本身就是一个指向hmap的指针   函数传参是值拷贝,传递进去的m 虽然拷贝了一份,但是他是指针,所以。。。。

func main(){
	fmt.Println("--------------- m ---------------")
	m := make(map[string]string)
	m["1"] = "0"
	fmt.Printf("m outer address %p, m=%p m=%v \n", &m,m,m)
	passMap(m)
	fmt.Printf("m outer address %p, m=%p m=%v \n", &m,m,m)
}
func passMap(m map[string]string) {
	fmt.Printf("m inner address %p m=%p m=%v \n", &m,m,m)
	m["11111111"] = "11111111"
	fmt.Printf("post m inner address %p m=%p m=%v \n",&m,m,m)
}
/*
--------------- m ---------------
m outer address 0xc000006030, m=0xc00005c6f0 m=map[1:0]
m inner address 0xc000006038 m=0xc00005c6f0 m=map[1:0]
post m inner address 0xc000006038 m=0xc00005c6f0 m=map[1:0 11111111:11111111]
m outer address 0xc000006030, m=0xc00005c6f0 m=map[1:0 11111111:11111111]
*/
/*
当传参为map的时候,其实传递的是指针地址。函数内外map的地址都是一样的。
函数内部的改变会透传到函数外部。
*/
func main(){
	fmt.Println("--------------- m ---------------")
	var m2 map[string]string//未初始化
	fmt.Printf("main11111m2 outer address=%p, m=%p,m=%v \n", &m2,m2,m2)
	passMapNotInit(m2)
	fmt.Printf("main22222m2 outer address=%p, m=%p,m=%v \n", &m2,m2,m2)
}
func passMapNotInit(m map[string]string)  {
	fmt.Printf("passMapNotInit11111: address=%p, m=%p,m=%v \n", &m,m,m)
	m = make(map[string]string, 0)
	m["a"]="11"
	fmt.Printf("passMapNotInit22222: address=%p, m=%p,m=%v \n", &m,m,m)
}
/*
--------------- m2 ---------------
main11111m2 outer address=0xc00008c020, m=0x0,m=map[]
passMapNotInit11111: address=0xc00008c028, m=0x0,m=map[]
passMapNotInit22222: address=0xc00008c028, m=0xc000068750,m=map[a:11]
main22222m2 outer address=0xc00008c020, m=0x0,m=map[]
*/
/*
没有初始化的map地址都是0;
函数内部初始化map不会透传到外部map。
因为map没有初始化,所以map的地址传递到函数内部之后初始化,会改变map的地址,但是外部地址不会改变。有一种方法,return 新建的map
*/

makemap这个函数返回的结果:hmap 是一个指针,而 makeslice 函数返回的是 Slice 结构体对象。这也是 makemap 和 makeslice 返回值的区别所带来一个不同点:当 map 和 slice 作为函数参数时,在函数参数内部对 map 的操作会影响 map 自身;而对 slice 却不会。主要原因:一个是指针(hmap),一个是结构体(slice)。Go 语言中的函数传参都是值传递,在函数内部,参数会被 copy 到本地。*hmap指针 copy 完之后,仍然指向同一个 map,因此函数内部对 map 的操作会影响实参。而 slice 被 copy 后,会成为一个新的 slice,对它进行的操作不会影响到实参

讲解map比较好的一个连接:Golang map实践以及实现原理_惜暮-CSDN博客_golang map实现原理

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值