Go 值传递与引用传递
在说 Go 的 值传递
与 引用传递
之前需要先了解一下 Go 的传递方式。
首先,Go 的变量有 T
与 *T
两种类型,某种意义上来说这两种参数传递的方式都是按值传递。为什么这么说呢?
当一个变量被声明为 T
类型。将其作为参数传递时,传递的是变量的副本。你会发现它的内存地址以及引用内存地址与原变量都是不一样的。
如果变量被声明为 *T
类型。传递变量时,会创建一个新的指针,同时这个指针会指向原变量的内存地址。所以这种传递方式可以看作是传递了一份变量引用地址的副本。
这就是为什么前面说 Go 的参数传递都是按值传递。只不过,T
传递的是变量的副本,而 *T
传递的是变量引用指针的副本。
T 与 *T 的传递方式
T类型变量的传递
首先看一下参数作为 T
的函数调用的情况:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func Rename(man Person) {
man.Name = "Tom"
fmt.Printf("改名后的 Person:%+v, 内存地址:%p\n", man, &man)
}
func main() {
jack := Person{Name: "Jack", Age: 20}
fmt.Printf("原始变量 Person:%+v, 内存地址:%p\n", jack, &jack)
Rename(jack)
fmt.Printf("调用改名函数后的原始 Person:%+v, 内存地址:%p\n", jack, &jack)
}
输出结果。
原始变量 Person:{Name:Jack Age:20}, 内存地址:0xc0000a6020
改名后的 Person:{Name:Tom Age:20}, 内存地址:0xc0000a6060
调用改名函数后的原始 Person:{Name:Jack Age:20}, 内存地址:0xc0000a6020
可以看到 Rename 函数中的 Person 与原始 Person 的指针地址不同,且 Rename 中修改 Person 的名称也不会影响原始值。
*T类型变量传递方式
修改上面的例子:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func Rename(man *Person) {
man.Name = "Tom"
fmt.Printf("改名后的 Person:%+v, 内存地址: %p, 指针地址:%p\n", man, man, &man)
}
func main() {
jack := &Person{Name: "Jack", Age: 20}
fmt.Printf("原始变量 Person:%+v, 内存地址:%p, 指针地址:%p\n", jack, jack, &jack)
Rename(jack)
fmt.Printf("调用改名函数后的原始 Person:%+v, 内存地址:%p, 指针地址:%p\n", jack, jack, &jack)
}
输出结果:
原始变量 Person:&{Name:Jack Age:20}, 内存地址:0xc00011a000, 指针地址:0xc000100018
改名后的 Person:&{Name:Tom Age:20}, 内存地址:0xc00011a000, 指针地址:0xc000100028
调用改名函数后的原始 Person:&{Name:Tom Age:20}, 内存地址:0xc00011a000, 指针地址:0xc000100018
可以看到,传递 *Person
给 Rename
函数时,会创建一个指针的副本 0xc000100028
,并且与 0xc000100018
一样,同时指向 0xc00011a000
,因为两个指针指向同一个内存地址,自然在 Rename
修改了 Name
之后,也会修改到原始值的 Name
字段。
T 还是 *T 要如何选择
一般来说需要考虑的是副本创建的成本与需求,主要有以下几点标准:
- 不想变量被函数修改则选择
T
。否则,则应该使用*T
定义。 - 如果变量是一个非常大的
struct
或者数组,则副本创建时会对性能有一定的影响。此时,就应该考虑使用*T
,在传递时,只会创建指针。对性能的影响几乎可以忽略不计。 - 如果变量只是在本函数内使用,不涉及到函数传递。则可以考虑使用
T
,这样 Go 编译器会尽量的把其声明到栈上,而*T
很可能会分配到堆上。会对垃圾回收有一定影响。
什么时候会发生副本创建
上面的例子都是说在作为函数参数传递的时候出现的副本创建的情况,其实还有很多其他情况下也会发生副本创建。
例子一:赋值
在将一个变量赋值给另一个变量时也会发生副本创建的情况,例如:
package main
import "fmt"
type Person struct {
Name string
Age int
}
var person1 = Person{Name: "jack", Age: 1}
var person2 = person1
func main() {
fmt.Printf("Person1: %v, 内存地址: %p\n", person1, &person1)
fmt.Printf("Person2: %v, 内存地址: %p\n", person2, &person2)
person3 := person2
fmt.Printf("Person3: %v, 内存地址: %p\n", person3, &person3)
person4 := person3
fmt.Printf("Person4: %v, 内存地址: %p\n", person4, &person4)
}
输出结果:
Person1: {jack 1}, 内存地址: 0x117ac20
Person2: {jack 1}, 内存地址: 0x117ac40
Person3: {jack 1}, 内存地址: 0xc00000c0a0
Person4: {jack 1}, 内存地址: 0xc00000c0e0
可以看到不管是在函数内还是函数外赋值,所有的变量的内存地址都是不同的。说明它们每次赋值都是创建了一个副本。
例子二:数组、slice 和 map
package main
import "fmt"
type Person struct {
Name string
Age int
}
var person = Person{Name: "jack", Age: 1}
func main() {
fmt.Printf("Person1: %v, 内存地址: %p\n", person, &person)
// slice
persons := []Person{person}
persons = append(persons, person)
fmt.Printf("Person2: %v, 内存地址: %p\n", persons[0], &(persons[0]))
fmt.Printf("Person3: %v, 内存地址: %p\n", persons[1], &(persons[1]))
// map
personMap := make(map[int]Person)
personMap[0] = person
person.Age = 2
fmt.Printf("Person4: %v\n", personMap[0])
person.Age = 3
person5 := personMap[0]
fmt.Printf("Person5: %v, 内存地址: %p\n", person5, &person5)
person.Age = 1
// array
a := [2]Person{person}
person.Age = 4
fmt.Printf("Person6: %v, 内存地址: %p\n", a[0], &a[0])
person.Age = 1
a[1] = person
person.Age = 6
fmt.Printf("Person7: %v, 内存地址: %p\n", a[1], &a[1])
}
执行结果:
Person1: {jack 1}, 内存地址: 0x117bc20
Person2: {jack 1}, 内存地址: 0xc000066180
Person3: {jack 1}, 内存地址: 0xc000066198
Person4: {jack 1}
Person5: {jack 1}, 内存地址: 0xc00000c100
Person6: {jack 1}, 内存地址: 0xc0000661b0
Person7: {jack 1}, 内存地址: 0xc0000661c8
可以看到 slice、map与数组的所有元素全都不同,说明它们都是原始变量的副本。
例子三:for-range
for-range 循环也是将元素的副本赋值到循环变量:
package main
import "fmt"
type Person struct {
Name string
Age int
}
var person = Person{Name: "jack", Age: 1}
func main() {
fmt.Printf("Person1: %v, 内存地址: %p\n", person, &person)
// slice
persons := []Person{person, person, person}
persons[0].Age = 2
persons[1].Age = 3
persons[2].Age = 4
person.Age = 5
for i, p := range persons {
fmt.Printf("Person%d: %v, 内存地址: %p\n", i, p, &p)
}
person.Age = 1
// map
personMap := make(map[int]Person)
person.Age = 1
personMap[0] = person
person.Age = 2
personMap[1] = person
person.Age = 3
personMap[2] = person
person.Age = 4
for k, p := range personMap {
fmt.Printf("Person%d: %v, 内存地址: %p\n", k, p, &p)
}
person.Age = 1
// array
persona := [3]Person{person, person, person}
persona[1].Age = 2
persona[2].Age = 3
person.Age = 4
for i, p := range persona {
fmt.Printf("Person%d: %v, 内存地址: %p\n", i, p, &p)
}
}
执行结果:
Person1: {jack 1}, 内存地址: 0x117bc20
Person0: {jack 2}, 内存地址: 0xc00000c080
Person1: {jack 3}, 内存地址: 0xc00000c080
Person2: {jack 4}, 内存地址: 0xc00000c080
Person1: {jack 2}, 内存地址: 0xc00000c100
Person2: {jack 3}, 内存地址: 0xc00000c100
Person0: {jack 1}, 内存地址: 0xc00000c100
Person0: {jack 1}, 内存地址: 0xc00000c180
Person1: {jack 2}, 内存地址: 0xc00000c180
Person2: {jack 3}, 内存地址: 0xc00000c180
for-range 会复用循环变量,所以它们的内存地址是一样的。
例子四:channel
向 channel
中发送数据时,也会创建对象的副本:
package main
import "fmt"
type Person struct {
Name string
Age int
}
var person = Person{Name: "jack", Age: 1}
func main() {
fmt.Printf("Person1: %v, 内存地址: %p\n", person, &person)
ch := make(chan Person, 3)
ch <- person
person.Age = 2
ch <- person
person.Age = 3
ch <- person
person.Age = 4
p := <-ch
fmt.Printf("Person2: %v, 内存地址: %p\n", p, &p)
p = <-ch
fmt.Printf("Person3: %v, 内存地址: %p\n", p, &p)
p = <-ch
fmt.Printf("Person4: %v, 内存地址: %p\n", p, &p)
}
执行结果:
Person1: {jack 1}, 内存地址: 0x117ac20
Person2: {jack 1}, 内存地址: 0xc0000a6040
Person3: {jack 2}, 内存地址: 0xc0000a6040
Person4: {jack 3}, 内存地址: 0xc0000a6040
例子五:函数的参数与返回值
将变量传递给函数,以及接收函数返回值的时候都会出现副本创建的情况:
package main
import "fmt"
type Person struct {
Name string
Age int
}
func AddAge(p Person) Person {
p.Age++
fmt.Printf("函数Person: %v, 内存地址: %p\n", p, &p)
return p
}
func main() {
person := Person{Name: "jack", Age: 1}
fmt.Printf("原始Person: %v, 内存地址: %p\n", person, &person)
p := AddAge(person)
fmt.Printf("函数执行后的原始Person: %v, 内存地址: %p\n", person, &person)
fmt.Printf("函数返回的Person: %v, 内存地址: %p\n", p, &p)
}
执行结果:
原始Person: {jack 1}, 内存地址: 0xc0000a6020
函数Person: {jack 2}, 内存地址: 0xc0000a6080
函数执行后的原始Person: {jack 1}, 内存地址: 0xc0000a6020
函数返回的Person: {jack 2}, 内存地址: 0xc0000a6060
可以看到传递进函数与接收函数返回结果时的值内存地址都是不同的,说明都发生了副本赋值的情况。
例子六:Method Receiver
因为方法会产生一个 Receiver
作为第一个参数传入,当 Receiver
为 T
时,会创建副本并调用副本上的方法。当 Receiver
为 *T
时,只创建对象的指针,不会创建对象本身,方法内对 Receiver
的改动会影响原值。
不同类型的副本创建
bool、数值和指针
bool和数值一般不必考虑指针类型,因为这些类型内存占用都非常小,创建副本的开销可以忽略不计。只有在想修改同一个变量值的时候才考虑使用指针。
指针类型也是类似的。
数组
数组是值类型,赋值的时候会发生原始数组的复制,所以对于大数组的参数传递和赋值,一定要注意。
package main
import "fmt"
func main() {
a1 := [3]int{1, 2, 3}
fmt.Printf("a1: %+v, 内存地址: %p\n", a1, &a1)
a2 := a1
a1[0] = 4
a1[1] = 5
a1[2] = 6
fmt.Printf("a2: %+v, 内存地址: %p\n", a2, &a2)
}
执行结果:
a1: [1 2 3], 内存地址: 0xc000014160
a2: [1 2 3], 内存地址: 0xc0000141c0
可以看到,将 a1
的值赋值给 a2
后两个数组的内存地址并不相同。且修改 a1
的值也并不会影响 a2
的值。
[...]T
与 [...]*T
在进行副本创建的时候复制的内容并不相同,[...]*T
的元素为 T
的内存指针。所以在副本创建的时候创建的副本元素也是所有元素的指针。
map、slice 和 channel
一般来说这三种类型都是执行指针类型,只想一个底层的数据结构。因此,在定义类型的时候不需要定义成 *T
。
当然这样认为也没错,不过他的数据是一个指针,所以它的副本创建对性能的影响可以忽略不计。
字符串
string
类型类似 slice
。所以很多情况下会用 unsafe.Pointer
与 []byte
类型进行更有效的转换。因为直接进行类型转换 string([]byte)
时会发生数据的复制。
字符串还有一个特殊的点,它的值不能修改,任何对字符串的修改都会生成新的字符串。
大部分情况下你不需要定义成 *string
。唯一的例外是你需要 nil
值的时候。string
的空值与缺省值为 ""
,但是如果你需要 nil
, 就必须定义为 *string
。例如,在对象序列化时 ""
和 nil
表示的意义时不一样的,""
表示字段存在,只不过字符串是空值,而 nil
则表示字段不存在。
函数
函数也是一个指针类型,对函数对象的赋值只是又创建了一个对函数对象的指针。
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)
}
执行结果:
f1: 0x10a0a90, 内存地址: 0xc00000e028
f2: 0x10a0a90, 内存地址: 0xc00000e038