介绍
在golang中只有值传递,没有引用传递。是哪个传递其实就是看值类型变量和引用类型变量作为函数参数时,修改形参是否会影响到实参。这里涉及到几个概念,下面分别介绍一下:
- 值传递:指在调用函数时将实参复制一份传递到函数中,这样在函数中如果对参数进行修改,将不会影响到实参。
- 引用传递:指在调用函数时将实参的地址直接传递到函数中,那么在函数中对参数所进行的修改,将影响到实参。
- 值类型:变量直接存储值,内存通常在栈上分配,栈在函数调用完会被释放。比如:int、float、bool、string、array、sturct 等。
- 引用类型:变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配,通过GC回收。比如:指针,slice,map,channel,interface,func 等。
- 深拷贝:值类型作为参数时,称为深拷贝,形参改变,实参不变,因为传递的是值的副本,形参会新开辟一块空间,与实参指向不同。如果希望值类型数据在修改形参时实参跟随变化,可以把参数设置为指针类型。
- 浅拷贝:引用类型作为参数时,称为浅拷贝,形参改变,实参跟随变化。因为传递的是地址,形参和实参都指向同一块地址。
在官方文档中有关于值传递的解释:https://go.dev/doc/faq#pass_by_value
与 C 家族中的所有语言一样,Go 中的所有内容都是按值传递的。也就是说,函数总是获取所传递内容的副本,就好像有一个赋值语句将值分配给参数一样。例如,将 int 值传递给函数会生成 int 的副本,传递指针值会生成指针的副本,但不会复制它指向的数据。
映射和切片值的行为类似于指针:它们是包含指向底层映射或切片数据的指针的描述符。复制map或切片的值不会复制它指向的数据。复制接口的值会复制存储在接口值中的内容。如果接口值包含一个结构体,则复制接口值会生成该结构体的副本。如果接口值包含一个指针,则复制接口值会复制该指针,但不会复制它所指向的数据。
示例
下面分别用示例演示。
值类型
基本类型
package main
import "fmt"
func modInt(x int) {
fmt.Printf("形参地址为%p,值是%v\n", &x, x)
x = 30
}
func main() {
a := 20
fmt.Printf("实参地址为%p,值是%v\n", &a, a)
modInt(a)
}
实参地址为0xc00001a098,值是20
形参地址为0xc00001a0d0,值是20
modInt 函数试图修改变量 x 的值,但是因为 x 是通过值传递的,所以它只是 a 的一个副本,a 的值在函数外部不会被改变,地址也不一样。
复合类型
package main
import "fmt"
type Person struct {
Name string
Age int
}
func modStruct(p Person) {
p.Age = 30
}
func main() {
p := Person{Name: "Tom", Age: 20}
fmt.Printf("实参地址为%p,值是%v\n", &p, p)
modStruct(p)
}
实参地址为0xc000008078,值是{Tom 20}
形参地址为0xc0000080a8,值是{Tom 20}
引用类型
切片
package main
import "fmt"
func modSlice(x []int) {
fmt.Printf("形参切片地址为%p,值是%v\n", &x, x)
fmt.Printf("形参切片第一个元素地址为%p,值是%v\n", &x[0], x[0])
x[0] = 30
}
func main() {
s := []int{10, 20, 30}
fmt.Printf("切片地址为%p,值是%v\n", &s, s)
fmt.Printf("切片第一个元素地址为%p,值是%v\n", &s[0], s[0])
modSlice(s)
fmt.Println(s)
}
切片地址为0xc000008078,值是[10 20 30]
切片第一个元素地址为0xc000010120,值是10
形参切片地址为0xc0000080a8,值是[10 20 30]
形参切片第一个元素地址为0xc000010120,值是10
[30 20 30]
虽然 modSlice 函数看似接收的是一个值(切片的副本),但是切片内部包含了指向底层数组的指针。slice本身是个结构体,但它内部第一个元素是一个指针类型,指向底层的具体数组,slice在传递时,形参是拷贝的实参这个slice,但他们底层指向的数组是一样的,拷贝slice时,其内部指针的值也被拷贝了,也就是说指针的内容一样,都是指向同一个数组,所以函数内部的修改会影响到原始的切片。
type slice struct {
array unsafe.Pointer
len int
cap int
}
map
package main
import "fmt"
func modMap(m map[string]int) {
fmt.Printf("形参地址为%p,值是%v\n", &m, m)
m["Tom"] = 30
}
func main() {
m := map[string]int{"Tom": 20}
fmt.Printf("实参地址为%p,值是%v\n", &m, m)
modMap(m)
fmt.Println(m)
}
实参地址为0xc00000a028,值是map[Tom:20]
形参地址为0xc00000a038,值是map[Tom:20]
map[Tom:30]
可以看到map中实参和形参的地址都不一样,说明传递的也是一份拷贝。
指针
package main
import "fmt"
func modPointer(x *int) {
fmt.Printf("形参地址为%p,值是%v\n", &x, *x)
*x = 30
}
func main() {
a := 20
fmt.Printf("实参地址为%p,值是%v\n", &a, a)
modPointer(&a)
}
实参地址为0xc00001a098,值是20
形参地址为0xc00000a030,值是20
30
可以看到指针中实参和形参的地址都不一样,说明传递的也是一份拷贝。输出30,因为a的值通过指针被修改了。
总结
无论是值类型还是引用类型,在函数调用时都是通过值传递的方式传递参数。
-
对于值类型,函数接收的是实际参数值的副本;
-
对于切片、映射、通道等引用类型,虽然看起来是值传递,但由于它们内部包含指向数据结构的指针,所以它们的行为更像是引用传递。函数接收的是指向实际参数值的指针的副本。这样可以保证函数的执行不会对原始参数产生副作用,并提供了更好的安全性和可预测性。