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实现原理