转自【http://colobu.com/2017/01/05/-T-or-T-it-s-a-question/】
在编程语言深入讨论中,经常被大家提起也是争论最多的讨论之一就是按值(by value)还是按引用传递(by reference, by pointer),你可以在C/C++或者Java的社区经常看到这样的讨论,也会看到很多这样的面试题。
对于Go语言,严格意义上来讲,只有一种传递,也就是按值传递(by value)。当一个变量当作参数传递的时候,会创建一个变量的副本,然后传递给函数或者方法,你可以看到这个副本的地址和变量的地址是不一样的。
当变量当做指针被传递的时候,一个新的指针被创建,它指向变量指向的同样的内存地址,所以你可以将这个指针看成原始变量指针的副本。当这样理解的时候,我们就可以理解成Go总是创建一个副本按值转递,只不过这个副本有时候是变量的副本,有时候是变量指针的副本。
这是Go语言中你理解后续问题的基础。
T*T[]T[]T[]*T
本文将详细介绍Go语言的变量的副本创建还是变量指针的副本创建的case以及各种类型在这些case的情况。
副本的创建
T*T
T的副本创建
T
运行后输入结果(每次运行指针的值可能不同):
TpassV
*T的副本创建
T*T
运行后输出结果:
passPp0xc4200740000xc4200740100xc420076000*T
T*T
T*T
T*TT*T
一般的判断标准是看副本创建的成本和需求。
T*T*TT*T
什么时候发生副本创建
上面举的例子都是作为函数参数时发生的副本的创建,还有很多情况下会发生副本的创建,甚至有些“隐蔽”的情况。
编程的时候如何小心这些情况呢,一条原则就是:
A go assignment is a copy of the value itself
赋值的时候就会创建对象副本
Assignment的语法表达式如下:
Assignment = ExpressionList assign_op ExpressionList .
assign_op = [ add_op | mul_op ] "=" .Each left-hand side operand must be addressable, a map index expression, or (for = assignments only) the blank identifier. Operands may be parenthesized.
最常见的case
最常见的赋值的例子是对变量的赋值,包括函数内和函数外:
输出结果:
可以看到这几个变量的内存地址都不相同,说明发生了赋值。
map、slice和数组
slice,map和数组在初始化和按索引设置的时候也会创建副本:
输出结果
可以看到 slice/map/数组 的元素全是原始变量的副本, 副本。
for-range循环
for-range循环也是将元素的副本赋值给循环变量,所以变量得到的是集合元素的副本。
输出结果
注意循环变量是重用的,所以你看到它们的地址是相同的。
channel
往channel中send对象的时候也会创建对象的副本:
输出结果:
函数参数和返回值
将变量作为参数传递给函数和方法会发生副本的创建。
对于返回值,将返回值赋值给其它变量或者传递给其它的函数和方法,就会创建副本。
Method Receiver
T*T
不同类型的副本创建
bool,数值和指针
bool和数值类型一般不必考虑指针类型,原因在于这些对象很小,创建副本的开销可以忽略。只有你在想修改同一个变量的值的时候才考虑它们的指针。
指针类型就不用多说了,和数值类型类似。
数组
数组是值类型,赋值的时候会发生原始数组的复制,所以对于大的数组的参数传递和赋值,一定要慎重。
输出
[...]T[...]*T[...]*T
map、slice 和 channel
*T
SliceHeaderData
字符串
StringHeaderstring([]byte)
字符串比较特殊,它的值不能修改,任何想对字符串的值做修改都会生成新的字符串。
*stringnilstring""nil*string""nil""nil
函数
函数也是一个指针类型,对函数对象的赋值只是又创建了一个对次函数对象的指针。
输出结果: