在编程语言深入讨论中,经常被大家提起也是争论最多的讨论之一就是按值(by value)还是按引用传递(by reference, by pointer),你可以在C/C++或者Java的社区经常看到这样的讨论,也会看到很多这样的面试题。
对于Go语言,严格意义上来讲,只有一种传递,也就是按值传递(by value)。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。
当变量当做指针被传递的时候,一个新的指针被创建,它指向变量指向的同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。当这样理解的时候,我们就可以理解成Go总是创建一个副本按值转递,只不过这个副本有时候是变量的副本,有时候是变量指针的副本。
这是Go语言中你理解后续问题的基础。
但是Go语言的情况比较复杂,我们什么时候选择 T
作为参数类型,什么时候选择 *T
作为参数类型? []T
是传递的指针还是值?选择[]T
还是[]*T
? 哪些类型复制和传递的时候会创建副本?什么情况下会发生副本创建?
本文将详细介绍Go语言的变量的副本创建还是变量指针的副本创建的case以及各种类型在这些case的情况。
1.副本的创建
前面已经讲到,T
类型的变量和*T
类型的变量在当做函数或者方法的参数时会传递它的副本。我们先看看例子。
1.1 T的副本创建
首先看一下 参数类型为T的函数调用的情况:
package main
import "fmt"
type Bird struct {
Age int
Name string
}
func passV(b Bird) {
b.Age++
b.Name = "Great" + b.Name
fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p\n", b, &b)
}
func main() {
parrot := Bird{Age: 1, Name: "Blue"}
fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p\n", parrot, &parrot)
passV(parrot)
fmt.Printf("调用后原始的Bird:\t %+v, \t\t内存地址:%p\n", parrot, &parrot)
}
运行后输入结果(每次运行指针的值可能不同):
原始的Bird: {Age:1 Name:Blue}, 内存地址:0xc420012260
传入修改后的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc4200122c0
调用后原始的Bird: {Age:1 Name:Blue}, 内存地址:0xc420012260
1.2 *T的副本创建
修改上面的例子,将函数的参数类型由T
改为*T
:
package main
import "fmt"
type Bird struct {
Age int
Name string
}
func passP(b *Bird) {
b.Age++
b.Name = "Great" + b.Name
fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *b, b, &b)
}
func main() {
parrot := &Bird{Age: 1, Name: "Blue"}
fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)
passP(parrot)
fmt.Printf("调用后原始的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)
}
运行后输出结果:
原始的Bird: {Age:1 Name:Blue}, 内存地址:0xc000004480, 指针的内存地址: 0xc000006028
传入修改后的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc000004480, 指针的内存地址: 0xc000006038
调用后原始的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc000004480, 指针的内存地址: 0xc000006028
可以看到在函数passP
中,参数p
是一个指向Bird的指针,传递参数给它的时候会创建指针的副本(0xc420074010),只不过指针0xc420074000
和0xc420074010
都指向内存地址0xc420076000
。 函数内对*T
的改变显然会影响原始的对象,因为它是对同一个对象的操作。
当然,一位对Go有深入了解的读者都已经对这个知识有所了解,也明白了T
和*T
作为参数的时候副本创建的不同。
2. 如何选择 T
和 *T
在定义函数和方法的时候,作为一位资深的Go开发人员,一定会对函数的参数和返回值定义成T
和*T
深思熟虑,有些情况下可能还会有些苦恼。
那么什么时候才应该把参数定义成类型T
,什么情况下定义成类型*T
呢。
一般的判断标准是看副本创建的成本和需求。
- 不想变量被修改。 如果你不想变量被函数和方法所修改,那么选择类型
T
。相反,如果想修改原始的变量,则选择*T
- 如果变量是一个大的struct或者数组,则副本的创建相对会影响性能,这个时候考虑使用
*T
,只创建新的指针,这个区别是巨大的 - (不针对函数参数,只针对本地变量/本地变量)对于函数作用域内的参数,如果定义成
T
,Go编译器尽量将对象分配到栈上,而*T
很可能会分配到对象上,这对垃圾回收会有影响