在编程语言深入讨论中,经常被大家提起也是争论最多的讨论之一就是按值(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的函数调用的情况:

  1. package main
  2. import "fmt"
  3. type Bird struct {
  4. Age int
  5. Name string
  6. }
  7. func passV(b Bird) {
  8. b.Age++
  9. b.Name = "Great" + b.Name
  10. fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p\n", b, &b)
  11. }
  12. func main() {
  13. parrot := Bird{Age: 1, Name: "Blue"}
  14. fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p\n", parrot, &parrot)
  15. passV(parrot)
  16. fmt.Printf("调用后原始的Bird:\t %+v, \t\t内存地址:%p\n", parrot, &parrot)
  17. }

运行后输入结果(每次运行指针的值可能不同):

  1. 原始的Bird: {Age:1 Name:Blue}, 内存地址:0xc420012260
  2. 传入修改后的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc4200122c0
  3. 调用后原始的Bird: {Age:1 Name:Blue}, 内存地址:0xc420012260

1.2 *T的副本创建

修改上面的例子,将函数的参数类型由T改为*T:

  1. package main
  2. import "fmt"
  3. type Bird struct {
  4. Age int
  5. Name string
  6. }
  7. func passP(b *Bird) {
  8. b.Age++
  9. b.Name = "Great" + b.Name
  10. fmt.Printf("传入修改后的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *b, b, &b)
  11. }
  12. func main() {
  13. parrot := &Bird{Age: 1, Name: "Blue"}
  14. fmt.Printf("原始的Bird:\t\t %+v, \t\t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)
  15. passP(parrot)
  16. fmt.Printf("调用后原始的Bird:\t %+v, \t内存地址:%p, 指针的内存地址: %p\n", *parrot, parrot, &parrot)
  17. }

运行后输出结果:

  1. 原始的Bird: {Age:1 Name:Blue}, 内存地址:0xc000004480, 指针的内存地址: 0xc000006028
  2. 传入修改后的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc000004480, 指针的内存地址: 0xc000006038
  3. 调用后原始的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc000004480, 指针的内存地址: 0xc000006028

可以看到在函数passP中,参数p是一个指向Bird的指针,传递参数给它的时候会创建指针的副本(0xc420074010),只不过指针0xc4200740000xc420074010都指向内存地址0xc420076000。 函数内对*T的改变显然会影响原始的对象,因为它是对同一个对象的操作。
当然,一位对Go有深入了解的读者都已经对这个知识有所了解,也明白了T*T作为参数的时候副本创建的不同。

2. 如何选择 T*T

在定义函数和方法的时候,作为一位资深的Go开发人员,一定会对函数的参数和返回值定义成T*T深思熟虑,有些情况下可能还会有些苦恼。
那么什么时候才应该把参数定义成类型T,什么情况下定义成类型*T呢。
一般的判断标准是看副本创建的成本和需求。

  1. 不想变量被修改。 如果你不想变量被函数和方法所修改,那么选择类型T。相反,如果想修改原始的变量,则选择*T
  2. 如果变量是一个的struct或者数组,则副本的创建相对会影响性能,这个时候考虑使用*T,只创建新的指针,这个区别是巨大的
  3. (不针对函数参数,只针对本地变量/本地变量)对于函数作用域内的参数,如果定义成T,Go编译器尽量将对象分配到栈上,而*T很可能会分配到对象上,这对垃圾回收会有影响