转自【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

函数

函数也是一个指针类型,对函数对象的赋值只是又创建了一个对次函数对象的指针。

输出结果: