Go 的值传递与引用传递

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

可以看到,传递 *PersonRename 函数时,会创建一个指针的副本 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 作为第一个参数传入,当 ReceiverT 时,会创建副本并调用副本上的方法。当 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
### Go语言中的参数传递方式 Go语言中所有的参数传递均基于值传递机制,即每次函数调用时都会创建实参的一个副本[^3]。这意味着当向函数传递基本数据类型的变量(如`int`, `float64`, 或者自定义的结构体)时,任何在函数体内对该参数所做的更改都不会反映到原始变量上。 然而,对于某些复杂的数据类型——比如指针、切片(`slice`)、映射表(`map`)信道(`chan`)——尽管它们也是按值传递,但实际上传递的是指向这些对象背后存储位置的引用或句柄。因此,在这种情况下,如果函数内部改变了由这些引用所指向的内容,则该变化同样会作用于外部可见的对象之上[^1]。 #### 示例代码展示不同的传递行为 以下是几个简单的例子来说明上述概念: ```go // 修改整形数值的例子 func modifyInt(x int) { x = 999 // 这里的改变只发生在本地副本中 } // 使用指针修改结构体成员 type Person struct { Name string } func updateName(p *Person, newName string) { p.Name = newName // 此处直接操作了原始person实例的名字字段 } // 对切片元素进行增删改查的操作 func alterSlice(s []int) { s[0] = 789 // 切片的第一个元素被更新 s = append(s, 1)// 向切片追加新元素 } ``` 在这个示例中,`modifyInt()` 函数接收一个整型作为输入并试图对其进行赋值操作,但这并不会影响到外界的实际变量;而 `updateName()` 接收了一个指向 `Person` 类型的指针,并能够成功地更名;最后,`alterSlice()` 展现了如何利用切片特性间接达到对外部数组内容的影响效果。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值