golang中传递中值传递以及指针传递

转自【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的副本创建

首先看一下 参数类型为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}, 内存地址:0xc420012260
传入修改后的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc4200122c0
调用后原始的Bird: {Age:1 Name:Blue}, 内存地址:0xc420012260

可以看到,在T类型作为参数的时候,传递的参数parrot会将它的副本(内存地址0xc4200122c0)传递给函数passV,在这个函数内对参数的改变不会影响原始的对象。

*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 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}, 内存地址:0xc420076000, 指针的内存地址: 0xc420074000
传入修改后的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc420076000, 指针的内存地址: 0xc420074010
调用后原始的Bird: {Age:2 Name:GreatBlue}, 内存地址:0xc420076000, 指针的内存地址: 0xc420074000

可以看到在函数passP中,参数p是一个指向Bird的指针,传递参数给它的时候会创建指针的副本(0xc420074010),只不过指针0xc4200740000xc420074010都指向内存地址0xc420076000。 函数内对*T的改变显然会影响原始的对象,因为它是对同一个对象的操作。

当然,一位对Go有深入了解的读者都已经对这个知识有所了解,也明白了T*T作为参数的时候副本创建的不同。

如何选择 T 和 *T

在定义函数和方法的时候,作为一位资深的Go开发人员,一定会对函数的参数和返回值定义成T*T深思熟虑,有些情况下可能还会有些苦恼。
那么什么时候才应该把参数定义成类型T,什么情况下定义成类型*T呢。

一般的判断标准是看副本创建的成本和需求。

  1. 不想变量被修改。 如果你不想变量被函数和方法所修改,那么选择类型T。相反,如果想修改原始的变量,则选择*T
  2. 如果变量是一个的struct或者数组,则副本的创建相对会影响性能,这个时候考虑使用*T,只创建新的指针,这个区别是巨大的
  3. (不针对函数参数,只针对本地变量/本地变量)对于函数作用域内的参数,如果定义成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.

最常见的case

最常见的赋值的例子是对变量的赋值,包括函数内和函数外:

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}, 内存地址:0xfa0a0
parrot2: {Age:1 Name:Blue}, 内存地址:0xfa0c0
parrot2: {Age:1 Name:Blue}, 内存地址:0xc42007e0c0
parrot4: {Age:1 Name:Blue}, 内存地址:0xc42007e100

可以看到这几个变量的内存地址都不相同,说明发生了赋值。

map、slice和数组

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循环

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}, 内存地址:0xfb0a0
parrot2: {Age:1 Name:Blue}, 内存地址:0xc4200122a0
parrot3: {Age:2 Name:Blue}, 内存地址:0xc4200122a0
parrot4: {Age:3 Name:Blue}, 内存地址:0xc4200122a0
parrot2: {Age:1 Name:Blue}, 内存地址:0xc420012320
parrot3: {Age:2 Name:Blue}, 内存地址:0xc420012320
parrot4: {Age:3 Name:Blue}, 内存地址:0xc420012320
parrot2: {Age:1 Name:Blue}, 内存地址:0xc4200123a0
parrot3: {Age:2 Name:Blue}, 内存地址:0xc4200123a0
parrot4: {Age:3 Name:Blue}, 内存地址:0xc4200123a0

注意循环变量是重用的,所以你看到它们的地址是相同的。

channel

往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}, 内存地址:0xfa0a0
parrot2: {Age:1 Name:Blue}, 内存地址:0xc4200122a0
parrot3: {Age:2 Name:Blue}, 内存地址:0xc4200122a0
parrot4: {Age:3 Name:Blue}, 内存地址:0xc4200122a0

函数参数和返回值

将变量作为参数传递给函数和方法会发生副本的创建。
对于返回值,将返回值赋值给其它变量或者传递给其它的函数和方法,就会创建副本。

Method Receiver

因为方法(method)最终会产生一个receiver作为第一个参数的函数(参看规范),所以就比较好理解method receiver的副本创建的规则了。
当receiver为T类型时,会发生创建副本,调用副本上的方法。
当receiver为*T类型时,只是会创建对象的指针,不创建对象的副本,方法内对receiver的改动会影响原始值。

不同类型的副本创建

bool,数值和指针

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], 内存地址:0xc420012260
a2: [1 2 3], 内存地址:0xc4200122c0

对于[...]T[...]*T的区别,我想你也应该清楚了,[...]*T创建的副本的元素时元数组元素指针的副本。

map、slice 和 channel

网上一般说, 这三种类型都是指向指针类型,指向一个底层的数据结构。
因此呢,在定义类型的时候就不必定义成*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

<think>首先,用户的问题是:“我想了解Golang中参数传递的方式,特别是是否采用值传递。请问golang参数传递机制值传递还是指针传递”。用户关注的是Golang的参数传递机制,特别是它是值传递还是指针传递。从系统指令中,我有一些约束:-使用中文回答。-生成相关问题。-回答中引用的段落末尾自然地添加引用标识,例如:[^1]。-所有行内数学表达式用$...$,独立公式用$$...$$格式(但在这个主题上,可能没有数学表达式)。-参考提供的引用,用户给了我几个引用:-[^1]:讨论Golang的值传递指针传递,特别提到切片中包含指针,实际上是值传递中的指针传递。-[^2]:解释了值传递引用传递的区别,并说Golang只有值传递,即使是传递指针,也是传递指针的拷贝。-[^3]:类似地,描述了值传递引用传递的定义。-[^4]:展示了通过指针传递修改值的示例。引用[^1]、[^2]、[^3][^4]都指向Golang的参数传递机制。关键点是:-Golang只有值传递。-这意味着函数参数总是被复制。-对于基本类型如int、float,传递的是值。-对于指针类型,传递的是指针的拷贝(值),但指针指向的内存是共享的。-对于切片、map、channel等,它们内部有指针,所以传递时复制的是描述符(descriptor),但数据在堆上是共享的。这与我作为AI的知识一致:在Go语言中,函数参数传递始终是值传递。即使是指针,也是传递指针的值(一个地址),而不是引用传递。我的回答需要结构清晰:-先回答问题:Golang使用值传递。-解释原因机制。-给出示例。-自然地添加引用标识。-生成相关问题。结构化回答:1.**引言**:直接回应问题。2.**解释机制**:-描述值传递是什么。-对比指针传递引用传递(但Golang没有引用传递)。-解释Golang中的实现:-基本类型:传递值。-指针类型:传递指针的值,可以修改指向的数据。-引用类型:如切片、map,它们类似于指针传递时复制描述符。-强调只有值传递。3.**引用支持**:自然地插入引用。-例如,引用[^1]说:“golang函数只有值传递,即使是传递指针,也是将指针拷贝一份”。-在相关段落后添加[^1]、[^2]等。4.**示例**:添加代码示例来展示。-如一个传递int值,另一个传递指针。5.**总结**:重申关键点。6.**生成相关问题**:在末尾添加,作为系统指令的一部分。生成相关问题:-系统指令说“生成相关问题”。-在用户可见层末尾添加。-基于主题:-例如:-Golang中的切片传递机制是怎样的?-为什么在Golang传递指针更高效?-如何避免在函数中意外修改数据?确保回答真实可靠:我将基于标准Golang知识。最终回答结构:-回答主体。-“§§相关问题§§”部分列出问题。-引用标识在回答中。使用中文。</think> ### Golang参数传递机制:值传递还是指针传递? 在Golang中,函数参数传递的方式**始终是值传递**。这意味着无论参数是什么类型(包括基本类型、指针或引用类型如切片或map),函数调用时都会创建一个参数值的拷贝,并将这个拷贝传递到函数内部。这与传统的引用传递不同,Golang没有直接支持引用传递的方式[^2][^3]。下面我将逐步解释Golang的参数传递机制,并辅以示例来说明。 #### 1. **值传递的本质** - **定义**:值传递(Pass by Value)意味着函数调用时,参数的**值被复制一份**,作为函数的参数。修改函数内部的参数(形参)不会影响原始变量(实参),除非使用指针。 - 例如,传递一个基本类型如`int`时,函数内对参数的修改不会改变外部变量。 - Golang的函数参数总是以这种方式工作,即使在传递指针或引用类型时也是如此,因为它复制的是指针值或描述符的拷贝[^1][^2]。 - **为什么是值传递**:这与Golang的设计哲学有关,它强调简单性安全性,避免意外副作用。C++等语言有引用传递,但Golang通过值传递配合指针来实现类似效果[^2][^4]。 #### 2. **值传递在不同类型中的表现** - **基本类型(如int、string)**: - 传递时,整个值的字节被复制。 - 函数内部修改参数不会影响外部变量。 - 示例代码: ```go package main import "fmt" func modifyValue(a int) { a = 20 // 只修改副本 } func main() { x := 10 modifyValue(x) fmt.Println(x) // 输出: 10,外部变量未变 } ``` 在示例中,`modifyValue`函数内部改变的是复制的值,外部变量`x`不受影响。这证实了值传递机制[^3]。 - **指针类型(如*int)**: - 参数传递的是指针的**值(地址)**,即指针本身的拷贝,而不是引用。 - 虽然指针值被复制,但指针指向的内存地址是相同的,因此函数内部可以通过指针修改外部数据。 - 示例代码(基于引用[^4]): ```go package main import "fmt" func modifyViaPointer(a *int) { *a = *a + 1 // 通过指针修改指向的值 } func main() { y := 1 modifyViaPointer(&y) fmt.Println(y) // 输出: 2,外部变量被修改 } ``` 这里,传递指针是地址值的拷贝(值传递),但由于指针指向相同的内存位置,外部变量`y`被改变。这就是所谓的“值传递中的指针传递”[^1]。 - **引用类型(如切片、map、channel)**: - 这些类型内部包含指针或其他引用机制(例如切片包含指向底层数组的指针),但传递时,**复制的是描述符(descriptor),而不是数据本身**。 - 函数内部修改引用类型(如添加元素到切片)会影响外部变量,因为描述符复制后指向的底层数据未变。 - 示例代码(切片传递): ```go package main import "fmt" func modifySlice(s []int) { s[0] = 100 // 修改底层数组 s = append(s, 5) // 不一定会影响外部,因为append可能创建新切片 } func main() { slice := []int{1, 2, 3} modifySlice(slice) fmt.Println(slice) // 输出: [100 2 3],因为修改了底层数组 } ``` 在这个案例中,切片`s`传递时被复制(值传递),但复制的描述符共享底层数组,所以数组内容被修改[^1][^3]。 #### 3. **为什么Golang没有引用传递** - 引用传递(Pass by Reference)在语言如C++中可直接传递地址并影响实参,但Golang通过值传递实现类似控制。 - 好处:提高代码可预测性(避免意外修改),简化内存管理。 - 缺点:对于大型对象,值传递可能导致性能问题(因为复制开销),此时建议用指针传递作为优化[^2][^4]。 #### 4. **关键总结** - Golang**只有值传递**,即所有参数传递都是创建值的拷贝。 - 指针引用类型在传递时也体现值传递指针是地址值的拷贝,但能通过解引用修改数据;引用类型传递描述符的拷贝,但共享底层数据。 - 这机制确保了代码安全性一致性,但开发者需根据需求选择使用指针(for 修改外部状态或性能)或直接值传递(for 避免副作用)[^1][^2]。 通过上述解释,您可以看到Golang的参数传递机制是值传递优先,配合指针来模拟部分引用功能。如果您有更多具体场景(如并发安全或性能优化),可以进一步讨论。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值