go map 的值传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package main

import "fmt"

func main() {
m := map[string]int{"hello": 1}
changeMapM(m)
fmt.Printf("After changeMapM, m1 is: %v\n", m)

n := map[string]int{"hello": 1}
changeMapN(n)
fmt.Printf("After changeMapN, m1 is: %v\n", n)
}

func changeMapM(m map[string]int) {
m["hello"] = 2
}

func changeMapN(n map[string]int) {
n = map[string]int{"hello": 2}
}

// 可以看到changeMapN并没有改变n的值,所以map是值传递
// output:
// After changeMapM, m1 is: map[hello:2]
// After changeMapN, m1 is: map[hello:1]

在 Go 语言中,map 是引用类型,但在函数参数传递时,仍然是按值传递的。这意味着当你将一个 map 传递给函数时,传递的是 map 引用的副本,而不是原始的引用。因此,在函数内部对 map 引用本身的重新赋值不会影响到原始的 map。

解释

changeMapM 函数:

传递的是 map 引用的副本。
在函数内部,通过引用修改 map 中的值,这会影响到原始的 map,因为引用指向的是同一个底层数据结构。

changeMapN 函数:

传递的是 map 引用的副本。
在函数内部,重新赋值 n,这只是改变了 n 引用的副本指向一个新的 map,不会影响到原始的 map。

解释

changeMapM 函数:

m[“hello”] = 2:修改了 map 中的值,因为 m 引用指向的是原始的 map。

changeMapN 函数:

n = map[string]int{“hello”: 2}:重新赋值 n,使其指向一个新的 map,但这不会影响到原始的 map,因为 n 只是引用的副本。

总结

在 Go 语言中,map 是引用类型,但函数参数传递是按值传递的。
在函数内部对 map 引用本身的重新赋值不会影响到原始的 map。
要修改原始的 map,需要通过引用修改其内容,而不是重新赋值引用。

go map 存储

在 Go 语言中,map 的底层实现使用了桶(bucket)来存储键值对。当桶中的键值对数量超过一定限制时,会创建溢出桶(overflow bucket)。此外,当 map 中的键值对数量增加到一定程度时,map 会进行扩容,以保持较低的哈希冲突率和较高的性能。

创建溢出桶(overflow bucket)
溢出桶是在以下情况下创建的:

桶已满:每个桶有一个固定的容量(通常是 8 个键值对)。当一个桶中的键值对数量超过这个容量时,会创建一个溢出桶来存储额外的键值对。
哈希冲突:当多个键的哈希值映射到同一个桶时,会发生哈希冲突。如果桶已满且发生哈希冲突,会创建溢出桶来存储冲突的键值对。

扩容

map 会在以下情况下进行扩容:

负载因子超过阈值:负载因子是 map 中键值对的数量与桶的数量之比。当负载因子超过某个阈值(通常是 6.5)时,map 会进行扩容。扩容时,map 的桶数量会增加一倍,以减少哈希冲突并提高性能。
溢出桶过多:如果 map 中有太多的溢出桶,说明哈希冲突较多,map 也会进行扩容,以减少溢出桶的数量。

总结
溢出桶:当桶中的键值对数量超过容量或发生哈希冲突时,会创建溢出桶。
扩容:当负载因子超过阈值或溢出桶过多时,map 会进行扩容,以减少哈希冲突并提高性能。
这些机制确保了 map 在存储大量键值对时仍能保持较高的性能和较低的哈希冲突率。

参考

If a map isn’t a reference variable, what is it?

there-is-no-pass-by-reference-in-go

https://dave.cheney.net/2015/12/07/are-go-maps-sensitive-to-data-races

https://victoriametrics.com/blog/go-map/#map-anatomy