在 Go 开发中,经常会遇到需要在函数中修改 map 或 slice
的场景。虽然它们都支持动态扩容,但在函数传参时的行为却大不相同。今天,让我们通过实例深入理解这个问题。
一个困惑的开始
看这样一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| func main() { m := map[string]int{"old": 1} modifyMap(m) fmt.Println(m)
s := []int{1, 2, 3} modifySlice(s) fmt.Println(s) }
func modifyMap(m map[string]int) { m["new"] = 1 delete(m, "old") }
func modifySlice(s []int) { s[0] = 100 s = append(s, 200) }
|
有趣的是:
- map 的所有操作都会影响原始数据
- slice 的简单索引修改会影响原始数据,但 append 可能不会
为什么会这样?让我们从内部结构开始分析。
内部结构解析
Map 的内部结构
1 2 3 4 5 6 7
| type hmap struct { count int flags uint8 B uint8 buckets unsafe.Pointer }
|
当我们声明一个 map 变量时:
1 2
| m := make(map[string]int)
|
Slice 的内部结构
1 2 3 4 5
| type slice struct { array unsafe.Pointer len int cap int }
|
当我们声明一个 slice 变量时:
1 2
| s := make([]int, 0, 10)
|
深入理解传参行为
场景一:简单修改(不涉及扩容)
1 2 3 4
| func modifyBoth(m map[string]int, s []int) { m["key"] = 1 s[0] = 100 }
|
图解:
1 2 3 4 5 6 7 8 9 10 11
| Map: main()中的 m -----> hmap{...} <----- modifyBoth()中的 m (同一个底层结构)
Slice: main()中的 s = slice{array: 指向数组1, len: 3, cap: 3} | v [1 2 3] ^ modifyBoth()中的 s = slice{array: 指向数组1, len: 3, cap: 3}
|
场景二:涉及扩容的操作
1 2 3 4 5 6 7 8 9
| func expandBoth(m map[string]int, s []int) { for i := 0; i < 100; i++ { m[fmt.Sprintf("key%d", i)] = i }
s = append(s, 200) }
|
图解:
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 28 29 30 31
| Map 扩容过程: Before: main()中的 m -----> hmap{buckets: 指向存储A} ^ expandBoth()中的 m ---------|
After: main()中的 m -----> hmap{buckets: 指向更大的存储B} // 同一个 hmap,只是更新了内部指针 ^ expandBoth()中的 m ---------|
Slice 扩容过程: Before: main()中的 s = slice{array: 指向数组A, len: 3, cap: 3} | v [1 2 3] ^ expandBoth()中的 s = slice{array: 指向数组A, len: 3, cap: 3}
After append: main()中的 s = slice{array: 指向数组A, len: 3, cap: 3} // 保持不变 | v [1 2 3]
expandBoth()中的 s = slice{array: 指向数组B, len: 4, cap: 6} // 新的结构体,指向新数组 | v [1 2 3 200]
|
关键区别解析
传递方式不同:
- map 传递的是指针,函数内外使用的是同一个 hmap 结构
- slice 传递的是结构体副本,函数内的修改发生在副本上
扩容行为不同:
- map 扩容时,原有的 hmap 结构保持不变,只更新内部的 buckets 指针
- slice 扩容时,会创建新的底层数组,并返回一个指向新数组的新 slice
结构体
修改效果不同:
- map 的所有操作(包括扩容)都会反映到原始数据
- slice 的行为分两种情况:
- 不涉及扩容的修改会影响原始数据(因为指向同一个底层数组)
- 涉及扩容的操作(如
append)会创建新的底层数组,修改不会影响原始数据
最佳实践
基于以上原理,在编码时应注意:
- 对于 map:
1 2 3
| func modifyMap(m map[string]int) { m["key"] = 1 }
|
- 对于 slice:
1 2 3 4 5 6 7
| func modifySlice(s []int) []int { return append(s, 1) }
s = modifySlice(s)
|
总结
理解 map 和 slice 的这些差异,关键在于:
- map 是指针类型,始终指向同一个 hmap 结构
- slice 是结构体,包含了指向底层数组的指针
- 扩容时 map 只更新内部指针,而 slice 需要创建新的底层数组
这种设计各有优势:
- map 的行为更加统一和直观
- slice 的设计提供了更多的灵活性和控制权
在实际编程中,正确理解和处理这些差异,是写出健壮 Go 代码的关键。