介绍
当您在 Go 中编写软件时,您将写函数和方法. 您将数据传输给这些函数作为 arguments。 有时,该函数需要数据的本地副本,并且您希望原始数据保持不变。 例如,如果您是银行,并且您有一个函数显示用户对其余额的变化取决于他们选择的储蓄计划,您不想在他们选择计划之前更改客户的实际余额;您只是想在计算中使用它。
其他时候,您可能希望该函数能够在原始变量中更改数据。例如,当银行客户向其帐户进行存款时,您希望存款函数能够访问实际余额,而不是副本。在这种情况下,您不需要将实际数据发送到函数;您只需要告诉函数数据所在位置在内存中。称为 pointer 的数据类型持有数据的内存地址,而不是数据本身。 内存地址告诉函数在哪里找到数据,而不是数据的值。
在本文中,您将创建和使用指针来共享变量存储空间的访问。
定义和使用指针
当你使用一个变量指针时,你需要了解几种不同的语法元素。 第一种是使用 ampersand(&
)。 如果你在变量名前放一个 ampersand,你表示你想要得到 address,或指针到那个变量。 第二种语法元素是使用星座(*
)或 dereferencing 操作员。 当你声明一个指针变量时,你按照指针指向的变量类型的变量名称,前缀为 *
,如下:
1var myPointer *int32 = &someint
这将myPointer
创建为一个int32
变量的指针,并将指针初始化为someint
的地址。
让我们来看看一个指向字符串
的指针,下面的代码声明一个字符串的值和一个字符串的指针:
1[label main.go]
2package main
3
4import "fmt"
5
6func main() {
7 var creature string = "shark"
8 var pointer *string = &creature
9
10 fmt.Println("creature =", creature)
11 fmt.Println("pointer =", pointer)
12}
使用以下命令运行程序:
1go run main.go
当你运行该程序时,它会打印变量的值,以及变量存储位置的地址(指针地址)。 记忆地址是一个六十进制的数字,而不是人可以读取的。 在实践中,你可能永远不会输出一个记忆地址来查看它。 我们为示范目的向你展示。 因为每一个程序在运行时在其自己的记忆空间中创建,指针的值每次运行时都会有所不同,并且将与这里显示的输出有所不同:
1[secondary_label Output]
2creature = shark
3pointer = 0xc0000721e0
我们定义的第一个变量被命名为生物
,并将其与鲨鱼
的值等同于字符串
。然后我们创建了另一个名为指针
的变量。这次,我们将指针
变量的值设置为生物
变量的地址。
这就是为什么当我们打印出‘指针’的值时,我们得到了‘0xc0000721e0’的值,这就是‘生物’变量目前存储在计算机内存的地址。
如果你想从指针
变量中打印指向的变量值,你需要 dereference 该变量. 下面的代码使用*
运算符来 dereference指针
变量并检索其值:
1[label main.go]
2
3package main
4
5import "fmt"
6
7func main() {
8 var creature string = "shark"
9 var pointer *string = &creature
10
11 fmt.Println("creature =", creature)
12 fmt.Println("pointer =", pointer)
13
14 fmt.Println("*pointer =", *pointer)
15}
如果您运行此代码,您将看到以下输出:
1[secondary_label Output]
2creature = shark
3pointer = 0xc000010200
4*pointer = shark
我们现在添加的最后一行引用了指针
变量,并打印了存储在该地址的值。
如果您想修改存储在指针
变量位置的值,您也可以使用 dereference 操作员:
1[label main.go]
2package main
3
4import "fmt"
5
6func main() {
7 var creature string = "shark"
8 var pointer *string = &creature
9
10 fmt.Println("creature =", creature)
11 fmt.Println("pointer =", pointer)
12
13 fmt.Println("*pointer =", *pointer)
14
15 *pointer = "jellyfish"
16 fmt.Println("*pointer =", *pointer)
17}
运行此代码以查看输出:
1[secondary_label Output]
2creature = shark
3pointer = 0xc000094040
4*pointer = shark
5*pointer = jellyfish
我们通过在变量名称前使用星座(*
)设置指针
变量所指的值,然后提供一个新的值jellyfish
。
你可能没有意识到,但我们实际上也改变了生物
变量的值,因为指针
变量实际上指向生物
变量的地址,这意味着如果我们改变了从生物
变量中指向的值,我们也会改变生物
变量的值。
1[label main.go]
2package main
3
4import "fmt"
5
6func main() {
7 var creature string = "shark"
8 var pointer *string = &creature
9
10 fmt.Println("creature =", creature)
11 fmt.Println("pointer =", pointer)
12
13 fmt.Println("*pointer =", *pointer)
14
15 *pointer = "jellyfish"
16 fmt.Println("*pointer =", *pointer)
17
18 fmt.Println("creature =", creature)
19}
结果看起来像这样:
1[secondary_label Output]
2creature = shark
3pointer = 0xc000010200
4*pointer = shark
5*pointer = jellyfish
6creature = jellyfish
虽然此代码说明了指针的运作方式,但这不是您在 Go 中使用指针的典型方式,更常见的是在定义函数参数和返回值时使用它们,或者在定义自定义类型的方法时使用它们。
再次,请记住,我们正在打印指针
的值,以说明它是指针,在实践中,您不会使用指针的值,而不是参考底层值来检索或更新该值。
函数指针接收器
当您编写函数时,您可以定义要通过 value 或 reference 传输的参数。通过 value 意味着将该值的副本发送到函数中,并且在该函数内的该参数中的任何更改都是 only 效应,而不是该函数传输的位置。
决定何时传递指针,而何时发送值,就是要知道你是否希望该值发生变化,如果你不希望该值发生变化,请将其发送为值,如果你想让您传输变量的函数能够改变它,那么你会将其传递为指针。
要看到差异,让我们先看看在一个参数中通过值
的函数:
1[label main.go]
2package main
3
4import "fmt"
5
6type Creature struct {
7 Species string
8}
9
10func main() {
11 var creature Creature = Creature{Species: "shark"}
12
13 fmt.Printf("1) %+v\n", creature)
14 changeCreature(creature)
15 fmt.Printf("3) %+v\n", creature)
16}
17
18func changeCreature(creature Creature) {
19 creature.Species = "jellyfish"
20 fmt.Printf("2) %+v\n", creature)
21}
结果看起来像这样:
1[secondary_label Output]
21) {Species:shark}
32) {Species:jellyfish}
43) {Species:shark}
首先,我们创建了一个名为生物
的自定义类型,它有一个名为物种
的字段,这是一个字符串。在主
函数中,我们创建了一个名为生物
的新类型的实例,并将物种
字段设置为鲨鱼
。
接下来,我们称之为changeCreature
,并输入creature
变量的副本。
函数改变生物
被定义为采取一个名为生物
的参数,它是我们之前定义的生物
类型,然后我们将物种
字段的值更改为jellyfish
并打印出来。
然而,当主要
函数的最后一行打印了生物
的值时,物种
的值仍然是鲨鱼
。 该值没有改变的原因是因为我们通过变量为 _value。
接下来,我们可以通过使用星座(*)的运算器将类型从
生物更改为指针,而不是将
生物更改,我们现在将指针转移到
生物或
生物中,在上一个例子中,
生物是一个具有
鲨鱼的
物种值的
结构`。
1[label main.go]
2package main
3
4import "fmt"
5
6type Creature struct {
7 Species string
8}
9
10func main() {
11 var creature Creature = Creature{Species: "shark"}
12
13 fmt.Printf("1) %+v\n", creature)
14 changeCreature(&creature)
15 fmt.Printf("3) %+v\n", creature)
16}
17
18func changeCreature(creature *Creature) {
19 creature.Species = "jellyfish"
20 fmt.Printf("2) %+v\n", creature)
21}
运行此代码以查看以下输出:
1[secondary_label Output]
21) {Species:shark}
32) &{Species:jellyfish}
43) {Species:jellyfish}
请注意,现在当我们在changeCreature
函数中将Species
值更改为jellyfish
时,它也会更改main
函数中定义的原始值。
因此,如果您希望函数能够更改一个值,则需要通过引用传递该值,要通过引用传递该值,您将指针传递给变量,而不是变量本身。
然而,有时您可能没有定义指针的实际值. 在这些情况下,程序中可能会出现 panic。
尼尔点击
Go 中的所有变量都有 zero 值。即使是指针也是如此. 如果您向类型声明指针,但不分配任何值,则零值将是null
。
在以下程序中,我们正在定义一个指针到一个生物
类型,但我们从来没有实例化一个生物
的实际实例,并将其地址分配给生物
指针变量。
1[label main.go]
2package main
3
4import "fmt"
5
6type Creature struct {
7 Species string
8}
9
10func main() {
11 var creature *Creature
12
13 fmt.Printf("1) %+v\n", creature)
14 changeCreature(creature)
15 fmt.Printf("3) %+v\n", creature)
16}
17
18func changeCreature(creature *Creature) {
19 creature.Species = "jellyfish"
20 fmt.Printf("2) %+v\n", creature)
21}
结果看起来像这样:
1[secondary_label Output]
21) <nil>
3panic: runtime error: invalid memory address or nil pointer dereference
4[signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86]
5
6goroutine 1 [running]:
7main.changeCreature(0x0)
8 /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18 +0x26
9 main.main()
10 /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13 +0x98
11 exit status 2
当我们运行该程序时,它打印了生物
变量的值,值是零
。然后我们称之为改变生物
函数,当该函数试图设置物种
字段的值时,它会惊慌。
在Go中,如果您收到一个论点作为指针,则在执行任何操作之前检查它是否为零,以防止程序陷入恐慌。
以下是检查nil
的常见方法:
1if someVariable == nil {
2 // print an error or return from the method or fuction
3}
如果你这样做,你可能只想返回,或者返回错误,以显示一个无效的参数被传递到函数或方法。
1[label main.go]
2package main
3
4import "fmt"
5
6type Creature struct {
7 Species string
8}
9
10func main() {
11 var creature *Creature
12
13 fmt.Printf("1) %+v\n", creature)
14 changeCreature(creature)
15 fmt.Printf("3) %+v\n", creature)
16}
17
18func changeCreature(creature *Creature) {
19 if creature == nil {
20 fmt.Println("creature is nil")
21 return
22 }
23
24 creature.Species = "jellyfish"
25 fmt.Printf("2) %+v\n", creature)
26}
我们在changeCreature
中添加了一个检查,看看creature
参数的值是否为null
。如果是,我们打印出creature is nil
,然后退出函数。
1[secondary_label Output]
21) <nil>
3creature is nil
43) <nil>
请注意,虽然我们仍然对生物
变量有零
值,但我们不再恐慌,因为我们正在检查这种情况。
最后,如果我们创建一个生物
类型的实例并将其分配给生物
变量,该程序将根据预期改变该值:
1[label main.go]
2package main
3
4import "fmt"
5
6type Creature struct {
7 Species string
8}
9
10func main() {
11 var creature *Creature
12 creature = &Creature{Species: "shark"}
13
14 fmt.Printf("1) %+v\n", creature)
15 changeCreature(creature)
16 fmt.Printf("3) %+v\n", creature)
17}
18
19func changeCreature(creature *Creature) {
20 if creature == nil {
21 fmt.Println("creature is nil")
22 return
23 }
24
25 creature.Species = "jellyfish"
26 fmt.Printf("2) %+v\n", creature)
27}
现在我们有一个生物
类型的实例,该程序将运行,我们将获得以下预期的输出:
1[secondary_label Output]
21) &{Species:shark}
32) &{Species:jellyfish}
43) &{Species:jellyfish}
要避免恐慌,您应该检查指针值是否为零
,然后尝试访问其中任何字段或方法。
接下来,让我们看看使用指针和值如何影响类型上的定义方法。
指针接收器方法
receiver in go 是方法声明中定义的参数,请查看以下代码:
1type Creature struct {
2 Species string
3}
4
5func (c Creature) String() string {
6 return c.Species
7}
该方法的接收器是c 生物
,它表示c
的实例是生物
类型,您将通过该实例变量引用该类型。
就像函数的行为不同,取决于您是否将参数发送为指针或值,方法也有不同的行为. 主要的区别是,如果您用值接收器定义方法,则无法对该方法定义的类型实例进行更改。
有时你希望你的方法能够更新你正在使用的变量实例. 要允许这一点,你想让接收器成为一个指针。
让我们在我们的生物
类型中添加一个重置
方法,将物种
字段设置为空串:
1[label main.go]
2package main
3
4import "fmt"
5
6type Creature struct {
7 Species string
8}
9
10func (c Creature) Reset() {
11 c.Species = ""
12}
13
14func main() {
15 var creature Creature = Creature{Species: "shark"}
16
17 fmt.Printf("1) %+v\n", creature)
18 creature.Reset()
19 fmt.Printf("2) %+v\n", creature)
20}
如果我们运行该程序,我们将获得以下输出:
1[secondary_label Output]
21) {Species:shark}
32) {Species:shark}
请注意,即使在重置
方法中,我们将物种
值设置为空串,但当我们在主
函数中打印生物
变量值时,该值仍然设置为鲨鱼
。
如果我们想能够在方法中修改生物
变量的实例,我们需要将它们定义为具有指针
接收器:
1[label main.go]
2package main
3
4import "fmt"
5
6type Creature struct {
7 Species string
8}
9
10func (c *Creature) Reset() {
11 c.Species = ""
12}
13
14func main() {
15 var creature Creature = Creature{Species: "shark"}
16
17 fmt.Printf("1) %+v\n", creature)
18 creature.Reset()
19 fmt.Printf("2) %+v\n", creature)
20}
请注意,当我们定义Reset
方法时,我们现在在Creature
类型前添加了一个星座(*
),这意味着将Reset
方法转移到Creature
方法的实例现在是一个指针,因此当我们进行更改时,它将影响该变量的原始实例。
1[secondary_label Output]
21) {Species:shark}
32) {Species:}
重置
方法现在已经改变了物种
字段的值。
结论
将函数或方法定义为通过 value 或通过 reference 将影响您的程序的哪些部分能够对其他部分进行更改。 控制该变量何时可以更改将使您能够编写更强大和可预测的软件。