爱上开源之golang入门至实战第四章函数(Func)(三)
4.4.3 按值传递(call by value) 按引用传递(call by reference)
Go 默认使用按值传递来传递参数,也就是传递参数的副本。函数接收参数副本之后,在使用变量的过程中可能会对副本 的值进行更改,但不会影响到原来的变量,比如 Function(arg1) 。 如果你希望函数可以直接修改参数的值,而不是对参数的副本进行操作,你需要将参数的地址(变量名前面添加&符 号,比如 &variable)传递给函数,这就是按引用传递,比如 Function(&arg1) ,此时传递给函数的是一个指针。 如果传递给函数的是一个指针,指针的值(一个地址)会被复制,但指针的值所指向的地址上的值不会被复制;我们 可以通过这个指针的值来修改这个指针所指向的地址上的值。(指针也是变量类型,有自己的地址和值,通常 指针的值指向一个变量的地址。所以,按引用传递也是按值传递。)
func exchange(a, b int) {
var temp int
fmt.Printf("Before exchanged: a=%d, b=%d\n", a, b)
temp = b
b = a
a = temp
fmt.Printf("After exchanged: a=%d, b=%d\n", a, b)
}
上方定义了一个exchange的函数,函数的传入参数是a,b两个int类型的数字, 函数交换两个参数的数值,这个函数的运行结果就可以充分地演示出按值传递的效果, 参数的参数都是副本, 所以执行结束以后,原有的a,b两个原值在函数体里都是在对副本进行的操作,函数体执行结束,a,b继续使用原值,所以执行完exchange以后,a,b值不会发生变化。
a, b := 1, 2
fmt.Printf("Before Func: a=%d, b=%d\n", a, b)
exchange(a, b)
fmt.Printf("After Func: a=%d, b=%d\n", a, b)
==== OUTPUT ====
Before Func: a=1, b=2
Before exchanged: a=1, b=2
After exchanged: a=2, b=1
After Func: a=1, b=2
结果输出可以看到,在执行之前a=1, b=2; 当进入函数体,两个原值都是用副本, 所以输出 a=1, b=2; 交换两个副本值,输出a=2, b=1;函数体结束,副本销毁,a,b使用原值,所以输出原值a=1, b=2;
我们可以改造一下exchange的函数体,来看看传入的两个参数的指针地址,是否和原来的一样。
func exchange2(a, b int) {
var temp int
fmt.Printf("Before exchanged: a[%p]=%d, b[%p]=%d\n", &a, a, &b, b)
temp = b
b = a
a = temp
fmt.Printf("After exchanged: a[%p]=%d, b[%p]=%d\n", &a, a, &b, b)
}
a, b := 1, 2
fmt.Printf("Before Func: a[%p]=%d, b[%p]=%d\n", &a, a, &b, b)
exchange2(a, b)
fmt.Printf("After Func: a[%p]=%d, b[%p]=%d\n", &a, a, &b, b)
==== OUTPUT ====
Before Func: a[0xc0002f4e08]=1, b[0xc0002f4e10]=2
Before exchanged: a[0xc0002f4e18]=1, b[0xc0002f4e20]=2
After exchanged: a[0xc0002f4e18]=2, b[0xc0002f4e20]=1
After Func: a[0xc0002f4e08]=1, b[0xc0002f4e10]=2
通过这里的代码测试,我们可以看到在函数调用前,和函数体里,函数体使用的是副本,指针地址不是原地址。 在函数体执行完成以后,变量a,b的地址又重新恢复成以前的指针地址,这就是我们所说的按值传递, 参数传递的是值得副本(拷贝对象)。
如果我们需要在上述的方法,确实一定要实现交换值的目标,我们可以通过传指针或者返回值覆盖原值的方式来实现; 代码如下:
传递指针
func exchange3(a, b *int) {
var temp int
fmt.Printf("Before exchanged: a[%p]=%d, b[%p]=%d\n", a, *a, b, *b)
temp = *b
*b = *a // 不能直接b = a 为什么?
*a = temp
fmt.Printf("After exchanged: a[%p]=%d, b[%p]=%d\n", a, *a, b, *b)
}
a, b := 1, 2
fmt.Printf("Before Func: a[%p]=%d, b[%p]=%d\n", &a, a, &b, b)
exchange3(&a, &b)
fmt.Printf("After Func: a[%p]=%d, b[%p]=%d\n", &a, a, &b, b)
==== OUTPUT ====
Before Func: a[0xc0002ece18]=1, b[0xc0002ece20]=2
Before exchanged: a[0xc0002ece18]=1, b[0xc0002ece20]=2
After exchanged: a[0xc0002ece18]=2, b[0xc0002ece20]=1
After Func: a[0xc0002ece18]=2, b[0xc0002ece20]=1
这段代码要细品;
品味1: 如何完成值交换的? // 不能直接b = a 为什么?
品味2:参数都换成了int类型的指针,注意点,指针的地址都是原来的地址?
返回值覆盖原值
func exchange4(a, b int) (int, int) {
var temp int
fmt.Printf("Before exchanged: a=%d, b=%d\n", a, b)
temp = b
b = a
a = temp
fmt.Printf("After exchanged: a=%d, b=%d\n", a, b)
return a, b
}
a, b := 1, 2
fmt.Printf("Before Func: a=%d, b=%d\n", a, b)
a, b = exchange4(a, b)
fmt.Printf("After Func: a=%d, b=%d\n", a, b)
==== OUTPUT ====
Before Func: a=1, b=2
Before exchanged: a=1, b=2
After exchanged: a=2, b=1
After Func: a=2, b=1
几乎在任何情况下,传递指针(一个32位或者64位的值)的消耗都比传递副本来得少。 调用都是更高效的调用,从基本上来说,至少传递指针的方式,只是一个32位或者64位值得复制,而指针传递的方式,就是要对传递的对象进行值拷贝。基本数据类型差异可能不明显,但是对于结构体,或者是数据量大的数组而言,就有很大的差异了。
在函数调用时,像切片(slice)、字典(map)、接口(interface)、函数(Func)、通道(channel)这样的引用类型都是默认使用引用传递(即使没有显式的指出指针)。
func exchange5(a []int) {
var temp int
fmt.Printf("Before exchanged: Array=%v\n", a)
temp = a[1]
a[1] = a[0]
a[0] = temp
fmt.Printf("After exchanged: Array=%v\n", a)
}
a := make([]int, 0)
a = append(a, 1, 2)
fmt.Printf("Before Func: Array=%v\n", a)
exchange5(a)
fmt.Printf("After Func: Array=%v\n", a)
==== OUTPUT ====
Before Func: Array=[1 2]
Before exchanged: Array=[1 2]
After exchanged: Array=[2 1]
After Func: Array=[2 1]
变量直接存储值,内存通常在栈中分配
变量存储的是一个地址,这个地址对应的空间才能真正存储数据(值); 内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收