问题描述
刚逛掘金的时候,看到一个问题, 说为什么我在函数体内修改了Slice的内容, 最后打印Slice的时候, Slice却没有变。
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
reverse(s)
fmt.Println(s)
}
func reverse(s []int) {
s = append(s, 999, 1000, 1001)
for i, j := 0, len(s)-1; i < j; i++ {
j = len(s) - (i + 1)
s[i], s[j] = s[j], s[i]
}
fmt.Println(s)
}
问题分析
-
因为 Go 遵循的是值拷贝传参的方式, reverse 函数入参的 s , 在 reverse 函数体内是局部变量, 和 main 函数中的 s 是两个不同的变量。
-
要继续分析 reverse 函数体内对 s 的修改, 是否会影响函数外 main 函数中 s 的值, 还需要对 Slice 结构体做进一步的分析。
# runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
从上面可以看出, slice 的结构里是直接保存了一个底层数组的指针。 当 reverse 函数体内执行 append 的时候, 变更了 reverse 函数体内的局部变量 s 持有的底层数组的指针,然后外部 main 函数的 s 持有的底层数组指针值并不会一起变化,应为他们是两个不同的变量。
- 如果 slice 的底层组织指针值没有变化的话,reverse 函数内是不是可以影响到 main 函数体的 s 变量的值? 答:是的
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
reverse(s)
fmt.Println(s)
}
func reverse(s []int) {
s[0], s[1] = s[1], s[0]
fmt.Println(s)
}
- Map 作为函数入参, 会发生什么呢? 首先, 我们先看创建 map 的时候发生了什么?
# runtime.map.go
// makemap implements Go map creation for make(map[k]v, hint).
// If the compiler has determined that the map or the first bucket
// can be created on the stack, h and/or bucket may be non-nil.
// If h != nil, the map can be created directly in h.
// If h.buckets != nil, bucket pointed to can be used as the first bucket.
func makemap(t *maptype, hint int, h *hmap) *hmap
如上面注释中所讲, 在执行 make(map[k]v, hint) 的时候, 实际调用的是 runtime/map.go中的 makemap 函数, 返回的是一个 *hmap 对象, 注意:返回的是一个指针对象, 也就是说 map 对象本身是一个 hmap 的指针类型。 所以在函数入参一个 map 对象的时候, 实际上是对 *hmap 对象的值拷贝。既然拷贝的本身就是一个指针, 那么函数入参中对 map 的操作, 等于只是对 *hmap 指向的 hmap 的操作。
- 所以我们接下来看看 hmap 的结构体:
# runtime.map.go
// A header for a Go map.
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/reflectdata/reflect.go.
// Make sure this stays in sync with the compiler's definition.
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
}
从上面可以看到 hmap 本身并不直接保存 map 的底层数组, 而是通过 buckets 等指针指向真正保存 map 数据的底层数据, 那么无论函数对入参 map 做的任何变化, 都只是影响 hmap 中 oldbuckets 等字段的值, 而 hmap 本身是不会变的, 那么 *hmap 也就永远指向的是同一个 hmap 对象, 那么函数传参前后, map都是同一个底层 hmap 对象, 所以函数对入参 map 的修改, 都会体现在入参前的 map 上。
简而言之: 把 map 作为一个函数入参, 其实拷贝的是一个 *hmap 对象, 它本身就是一个指针,所以, 函数内对 map 的操作, 都会影响函数外原本的 map。 这里根本不需要去分析 map 和 hmap , bmap 复杂的初始化, 赋值, 扩容的过程。 只需要知道一点就行了 map == *hmap 。