问题
Golang 中向函数传参,是值传递还是引用传递呢? 先抛结论:值传递,拷贝的是值的副本。
试验
有时看起来会“不符合预期”,以下通过代码进行说明,进而了解 Go 类型的值类型和引用类型。
package main
import (
"fmt"
)
func main() {
s := []int{1, 2, 3, 4, 5}
fmt.Printf("%p %v\n", s, s)
modify(s)
fmt.Printf("%p %v\n", s, s)
}
func modify(s2[]int) {
fmt.Printf("%p %v\n", s2, s2)
s2 = s2[1:3]
s2[0] = 100
fmt.Printf("%p %v\n", s2, s2)
}
输出
0xc000122060 [1 2 3 4 5]
0xc000122060 [1 2 3 4 5]
0xc000122068 [100 3]
0xc000122060 [1 100 3 4 5]
以传值解释看似“解释不通”,第二个 Printf
的打印的 s2 应该是不同的地址才对,但现在它跟 s 的地址是相同的,更 “离谱” 的是第四个打印的 s 值都被修改了,怎么看都像是引用,怎么说 Go 中只有值传递呢
这是因为 Golang 中数据类型可以分为两种,一种是值类型(int、float、bool、struct 等),另一种是引用类型(slice、map、channel、接口等),值类型的变量存储的是值,而引用类型的变量内存储着一个内存地址,这个内存地址存储着数据。
以 Slice
类型为例,指针地址指向着这个 slice 的第一个元素。
当向函数内传参时触发拷贝,值类型创建新变量,拷贝了数据,引用类型也创建了新变量,拷贝的是包含指向数据指针的数据,指针指向的是同一个内存地址,这也就是为什么修改 s2 也会影响到 s 的原因。
另外,第三行输出的地址是 slice 中第三个元素的地址。
源代码中 slice 是一个结构体,slice 变量不仅存储指针,也存储了描述数据长度了变量容量的 len 和 cap
// https://github.com/golang/go/blob/master/src/runtime/slice.go
type slice struct {
array unsafe.Pointer
len int
cap int
}
再写一个 int 类型的传值看看结果
func main() {
a := 1
fmt.Printf("%p %v\n", &a, a)
modify(a)
fmt.Printf("%p %v\n", &a, a)
}
func modify(a2 int) {
fmt.Printf("%p %v\n", &a2, a2)
a2 = 100
fmt.Printf("%p %v\n", &a2, a2)
}
结果
0xc0000b2008 1
0xc0000b2020 1
0xc0000b2020 100
0xc0000b2008 1
因为值类型的变量直接存储数据,相当于拷贝了数据,存在不同的内存地址,所以它们的地址不一样,对函数内变量值的修改自然也不会影响到函数外部的变量。
在回到 Slice 示例,上方 slice 结构体我们知道 slice 类型不仅保存着指向数据的指针,还保存着 len 和 cap,当发生拷贝时,三个值都进行了复制,len 和 cap 因为是 int 类型的数据,结合 int 类型传值示例,如果在函数内部 len 和 cap 发生了改变,有理由 “猜测” 不会影响到函数外部变量的 len 和 cap。
func main() {
s := make([]int, 3, 4)
s[0] = 1
s[1] = 2
s[2] = 3
fmt.Printf("%p %v %d %d\n", s, s, len(s), cap(s))
// fmt.Printf("%p %v %d %d\n", s, s[:4], len(s), cap(s))
modify(s)
fmt.Printf("%p %v %d %d\n", s, s, len(s), cap(s))
// fmt.Printf("%p %v %d %d\n", s, s[:4], len(s), cap(s))
}
func modify(s2[]int) {
fmt.Printf("%p %v %d %d\n", s2, s2, len(s2), cap(s2))
s2 = append(s2, 4)
fmt.Printf("%p %v %d %d\n", s2, s2, len(s2), cap(s2))
}
结果
0xc000018100 [1 2 3] 3 4
0xc000018100 [1 2 3] 3 4
0xc000018100 [1 2 3 4] 4 4
0xc000018100 [1 2 3] 3 4
通过示例可以看到地址没有变化,而且正如猜测,append 操作后,s2 的 len 值改变但外部的 len 没有变,结合以上的示例,还有更有意思的地方,取消两行 Printf
代码注释,使用 s[:4]
强制输出 slice 第四个元素,结果如下
0xc000136000 [1 2 3] 3 4
0xc000136000 [1 2 3 0] 3 4 -- 新增
0xc000136000 [1 2 3] 3 4
0xc000136000 [1 2 3 4] 4 4
0xc000136000 [1 2 3] 3 4
0xc000136000 [1 2 3 4] 3 4 -- 新增
可以看到在申请 slice 时候指定 cap: 4,第四个元素默认为 0,在函数中通过 append 后,第四个位置被赋值为 4,在函数外原始 s 的 len 值没有变,但 s 的第四位数据变成了 4,因为 len 的限制,如果不强制输出,没有打印出来。 需要注意的是,当 cap 不够的时候,Golang 会按照 slice 扩容规则创建一个新的内存地址,将数据复制过去。
s2 = append(s2, 4, 5) // 添加一个元素 5
做如上改动后,再运行程序,输出如下:
0xc00012c000 [1 2 3] 3 4
0xc00012c000 [1 2 3 0] 3 4
0xc00012c000 [1 2 3] 3 4
0xc000130000 [1 2 3 4 5] 5 8
0xc00012c000 [1 2 3] 3 4
0xc00012c000 [1 2 3 0] 3 4
可见在函数中 append 赋值操作后 s2 拥有了自己的数据地址,对其修改不会影响函数外的数据。
那么最后,只需要记住一个结论:
结论
在 Golang 中只有值传递(要么是该值的副本,要么是指针的副本),不存在引用传递。
参考