转自【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语言中你理解后续问题的基础。
但是Go语言的情况比较复杂,我们什么时候选择 T
作为参数类型,什么时候选择 *T
作为参数类型? []T
是传递的指针还是值?选择[]T
还是[]*T
? 哪些类型复制和传递的时候会创建副本?什么情况下会发生副本创建?
本文将详细介绍Go语言的变量的副本创建还是变量指针的副本创建的case以及各种类型在这些case的情况。
前面已经讲到,T
类型的变量和*T
类型的变量在当做函数或者方法的参数时会传递它的副本。我们先看看例子。
首先看一下 参数类型为T
的函数调用的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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)
}
|
运行后输入结果(每次运行指针的值可能不同):
1
2
3
|
原始的
Bird:
{Age:1 Name:Blue}, 内存地址:0
xc420012260
传入修改后的
Bird:
{Age:2 Name:GreatBlue}, 内存地址:0
xc4200122c0
调用后原始的
Bird:
{Age:1 Name:Blue}, 内存地址:0
xc420012260
|
可以看到,在T
类型作为参数的时候,传递的参数parrot会将它的副本(内存地址0xc4200122c0)传递给函数passV
,在这个函数内对参数的改变不会影响原始的对象。
修改上面的例子,将函数的参数类型由T
改为*T
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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)
}
|
运行后输出结果:
1
2
3
|
原始的
Bird:
{Age:1 Name:Blue}, 内存地址:0
xc420076000, 指针的内存地址: 0
xc420074000
传入修改后的
Bird:
{Age:2 Name:GreatBlue}, 内存地址:0
xc420076000, 指针的内存地址: 0
xc420074010
调用后原始的
Bird:
{Age:2 Name:GreatBlue}, 内存地址:0
xc420076000, 指针的内存地址: 0
xc420074000
|
可以看到在函数passP
中,参数p
是一个指向Bird的指针,传递参数给它的时候会创建指针的副本(0xc420074010),只不过指针0xc420074000
和0xc420074010
都指向内存地址0xc420076000
。 函数内对*T
的改变显然会影响原始的对象,因为它是对同一个对象的操作。
当然,一位对Go有深入了解的读者都已经对这个知识有所了解,也明白了T
和*T
作为参数的时候副本创建的不同。
T
和 *T
在定义函数和方法的时候,作为一位资深的Go开发人员,一定会对函数的参数和返回值定义成T
和*T
深思熟虑,有些情况下可能还会有些苦恼。
那么什么时候才应该把参数定义成类型T
,什么情况下定义成类型*T
呢。
一般的判断标准是看副本创建的成本和需求。
T
。相反,如果想修改原始的变量,则选择*T
*T
,只创建新的指针,这个区别是巨大的T
,Go编译器尽量将对象分配到栈上,而*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.
最常见的赋值的例子是对变量的赋值,包括函数内和函数外:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
package main
import
"fmt"
type Bird
struct {
Age
int
Name
string
}
type Parrot
struct {
Age
int
Name
string
}
var parrot1 = Bird{Age:
1, Name:
"Blue"}
var parrot2 = parrot1
func main() {
fmt.Printf(
"parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)
fmt.Printf(
"parrot2:\t\t %+v, \t\t内存地址:%p\n", parrot2, &parrot2)
parrot3 := parrot1
fmt.Printf(
"parrot2:\t\t %+v, \t\t内存地址:%p\n", parrot3, &parrot3)
parrot4 := Parrot(parrot1)
fmt.Printf(
"parrot4:\t\t %+v, \t\t内存地址:%p\n", parrot4, &parrot4)
}
|
输出结果:
1
2
3
4
|
parrot1:
{Age:1 Name:Blue}, 内存地址:0
xfa0a0
parrot2:
{Age:1 Name:Blue}, 内存地址:0
xfa0c0
parrot2:
{Age:1 Name:Blue}, 内存地址:0
xc42007e0c0
parrot4:
{Age:1 Name:Blue}, 内存地址:0
xc42007e100
|
可以看到这几个变量的内存地址都不相同,说明发生了赋值。
slice,map和数组在初始化和按索引设置的时候也会创建副本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
|
package main
import
"fmt"
type Bird
struct {
Age
int
Name
string
}
var parrot1 = Bird{Age:
1, Name:
"Blue"}
func main() {
fmt.Printf(
"parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)
//slice
s := []Bird{parrot1}
s =
append(s, parrot1)
parrot1.Age =
3
fmt.Printf(
"parrot2:\t\t %+v, \t\t内存地址:%p\n", s
[0], &(s
[0]))
fmt.Printf(
"parrot3:\t\t %+v, \t\t内存地址:%p\n", s
[1], &(s
[1]))
parrot1.Age =
1
//map
m :=
make(
map[
int]Bird)
m
[0] = parrot1
parrot1.Age =
4
fmt.Printf(
"parrot4:\t\t %+v\n", m
[0])
parrot1.Age =
5
parrot5 := m
[0]
fmt.Printf(
"parrot5:\t\t %+v, \t\t内存地址:%p\n", parrot5, &parrot5)
parrot1.Age =
1
//array
a :=
[2]Bird{parrot1}
parrot1.Age =
6
fmt.Printf(
"parrot6:\t\t %+v, \t\t内存地址:%p\n", a
[0], &a
[0])
parrot1.Age =
1
a
[1] = parrot1
parrot1.Age =
7
fmt.Printf(
"parrot7:\t\t %+v, \t\t内存地址:%p\n", a
[1], &a
[1])
}
|
输出结果
1
2
3
4
5
6
7
|
parrot1: {Age
:1 Name:Blue}, 内存地址
:0xfa0a0
parrot2: {Age
:1 Name:Blue}, 内存地址
:0xc4200160f0
parrot3: {Age
:1 Name:Blue}, 内存地址
:0xc420016108
parrot4: {Age
:1 Name:Blue}
parrot5: {Age
:1 Name:Blue}, 内存地址
:0xc420012320
parrot6: {Age
:1 Name:Blue}, 内存地址
:0xc420016120
parrot7: {Age
:1 Name:Blue}, 内存地址
:0xc420016138
|
可以看到 slice/map/数组 的元素全是原始变量的副本, 副本。
for-range循环也是将元素的副本赋值给循环变量,所以变量得到的是集合元素的副本。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
package main
import
"fmt"
type Bird
struct {
Age
int
Name
string
}
var parrot1 = Bird{Age:
1, Name:
"Blue"}
func main() {
fmt.Printf(
"parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)
//slice
s := []Bird{parrot1, parrot1, parrot1}
s
[0].Age =
1
s
[1].Age =
2
s
[2].Age =
3
parrot1.Age =
4
for i, p :=
range s {
fmt.Printf(
"parrot%d:\t\t %+v, \t\t内存地址:%p\n", (i +
2), p, &p)
}
parrot1.Age =
1
//map
m :=
make(
map[
int]Bird)
parrot1.Age =
1
m
[0] = parrot1
parrot1.Age =
2
m
[1] = parrot1
parrot1.Age =
3
m
[2] = parrot1
parrot1.Age =
4
for k, v :=
range m {
fmt.Printf(
"parrot%d:\t\t %+v, \t\t内存地址:%p\n", (k +
2), v, &v)
}
parrot1.Age =
4
//array
a := [...]Bird{parrot1, parrot1, parrot1}
a
[0].Age =
1
a
[1].Age =
2
a
[2].Age =
3
parrot1.Age =
4
for i, p :=
range a {
fmt.Printf(
"parrot%d:\t\t %+v, \t\t内存地址:%p\n", (i +
2), p, &p)
}
}
|
输出结果
1
2
3
4
5
6
7
8
9
10
|
parrot1:
{Age:1 Name:Blue}, 内存地址:0
xfb0a0
parrot2:
{Age:1 Name:Blue}, 内存地址:0
xc4200122a0
parrot3:
{Age:2 Name:Blue}, 内存地址:0
xc4200122a0
parrot4:
{Age:3 Name:Blue}, 内存地址:0
xc4200122a0
parrot2:
{Age:1 Name:Blue}, 内存地址:0
xc420012320
parrot3:
{Age:2 Name:Blue}, 内存地址:0
xc420012320
parrot4:
{Age:3 Name:Blue}, 内存地址:0
xc420012320
parrot2:
{Age:1 Name:Blue}, 内存地址:0
xc4200123a0
parrot3:
{Age:2 Name:Blue}, 内存地址:0
xc4200123a0
parrot4:
{Age:3 Name:Blue}, 内存地址:0
xc4200123a0
|
注意循环变量是重用的,所以你看到它们的地址是相同的。
往channel中send对象的时候也会创建对象的副本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import
"fmt"
type Bird
struct {
Age
int
Name
string
}
var parrot1 = Bird{Age:
1, Name:
"Blue"}
func main() {
ch :=
make(
chan Bird,
3)
fmt.Printf(
"parrot1:\t\t %+v, \t\t内存地址:%p\n", parrot1, &parrot1)
ch <- parrot1
parrot1.Age =
2
ch <- parrot1
parrot1.Age =
3
ch <- parrot1
parrot1.Age =
4
p := <-ch
fmt.Printf(
"parrot%d:\t\t %+v, \t\t内存地址:%p\n",
2, p, &p)
p = <-ch
fmt.Printf(
"parrot%d:\t\t %+v, \t\t内存地址:%p\n",
3, p, &p)
p = <-ch
fmt.Printf(
"parrot%d:\t\t %+v, \t\t内存地址:%p\n",
4, p, &p)
}
|
输出结果:
1
2
3
4
|
parrot1:
{Age:1 Name:Blue}, 内存地址:0
xfa0a0
parrot2:
{Age:1 Name:Blue}, 内存地址:0
xc4200122a0
parrot3:
{Age:2 Name:Blue}, 内存地址:0
xc4200122a0
parrot4:
{Age:3 Name:Blue}, 内存地址:0
xc4200122a0
|
将变量作为参数传递给函数和方法会发生副本的创建。
对于返回值,将返回值赋值给其它变量或者传递给其它的函数和方法,就会创建副本。
因为方法(method)最终会产生一个receiver作为第一个参数的函数(参看规范),所以就比较好理解method receiver的副本创建的规则了。
当receiver为T
类型时,会发生创建副本,调用副本上的方法。
当receiver为*T
类型时,只是会创建对象的指针,不创建对象的副本,方法内对receiver的改动会影响原始值。
bool和数值类型一般不必考虑指针类型,原因在于这些对象很小,创建副本的开销可以忽略。只有你在想修改同一个变量的值的时候才考虑它们的指针。
指针类型就不用多说了,和数值类型类似。
数组是值类型,赋值的时候会发生原始数组的复制,所以对于大的数组的参数传递和赋值,一定要慎重。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package main
import
"fmt"
func main() {
a1 :=
[3]
int
{1,
2,
3}
fmt.Printf(
"a1:\t\t %+v, \t\t内存地址:%p\n", a1, &a1)
a2 := a1
a1
[0] =
4
a1
[1] =
5
a1
[2] =
6
fmt.Printf(
"a2:\t\t %+v, \t\t内存地址:%p\n", a2, &a2)
}
|
输出
1
2
|
a1:
[1 2 3], 内存地址:0
xc420012260
a2:
[1 2 3], 内存地址:0
xc4200122c0
|
对于[...]T
和[...]*T
的区别,我想你也应该清楚了,[...]*T
创建的副本的元素时元数组元素指针的副本。
网上一般说, 这三种类型都是指向指针类型,指向一个底层的数据结构。
因此呢,在定义类型的时候就不必定义成*T
了。
当然你可以这么认为,不过我认为这是不准确的,比如slice,其实你可以看成是SliceHeader
对象,只不过它的数据Data
是一个指针,所以它的副本的创建对性能的影响可以忽略。
string类型类似slice,它等价StringHeader
。所以很多情况下会用`unsafe.Pointer`与[]byte类型进行更有效的转换,因为直接进行类型转换string([]byte)
会发生数据的复制。
字符串比较特殊,它的值不能修改,任何想对字符串的值做修改都会生成新的字符串。
大部分情况下你不需要定义成*string
。唯一的例外你需要 nil
值的时候。我们知道,类型string
的空值/缺省值为""
,但是如果你需要nil
,你就必须定义*string
。举个例子,在对象序列化的时候""
和nil
表示的意义是不一样的,""
表示字段存在,只不过字符串是空值,而nil
表示字段不存在。
函数也是一个指针类型,对函数对象的赋值只是又创建了一个对次函数对象的指针。
1
2
3
4
5
6
7
8
9
10
11
|
package main
import
"fmt"
func main() {
f1 :=
func(i
int) {}
fmt.Printf(
"f1:\t\t %+v, \t\t内存地址:%p\n", f1, &f1)
f2 := f1
fmt.Printf(
"f2:\t\t %+v, \t\t内存地址:%p\n", f2, &f2)
}
|
输出结果:
1
2
|
f1:
0x2200, 内存地址:
0xc420028020
f2:
0x2200, 内存地址:
0xc420028030
|