引言

当你在围棋中编写软件的时候,你需要写函数和方法。 将数据作为参数传递给这些函数。 有时,函数需要数据的本地副本,而您希望原始数据保持不变。 例如,如果你是一家银行,你有一个向用户显示根据他们选择的储蓄计划而改变他们余额的功能,你不想在客户选择计划之前改变他们的实际余额; 你只想在计算中使用它。 这被称为通过值传递,因为您将变量的值发送给函数,而不是变量本身。

其他时候,您可能希望函数能够更改原始变量中的数据。 例如,当银行客户将存款存入他们的帐户时,您希望存款函数能够访问实际余额,而不是副本。 在这种情况下,您不需要向函数发送实际的数据; 您只需要告诉函数数据在内存中的位置。 称为指针的数据类型保存数据的内存地址,但不保存数据本身。 内存地址告诉函数在哪里可以找到数据,而不是数据的值。 您可以将指针传递给函数,而不是传递数据,然后函数可以就地修改原始变量。 这被称为通过引用传递,因为变量的值并没有传递给函数,只是传递给了它的位置。

在本文中,您将创建并使用指针来共享对变量的内存空间的访问。

定义和使用指针

当您使用指向变量的指针时,需要理解几个不同的语法元素。 第一个是与号(&)的使用。 如果在变量名称前面放置一个与号,则表示希望获取地址或指向该变量的指针。 第二个语法元素是使用 asterisk (*)或解引用操作符。 当你声明一个指针变量时,你可以在变量名后面加上指针指向的变量的类型,前面加上 * ,像这样:

var myPointer *int32 = &someint

这将创建 myPointer 作为一个 int32变量的指针,并用 someint 的地址初始化该指针。 指针实际上并不包含 int32,只包含一个 int32的地址。

让我们看一下指向字符串的指针。 下面的代码声明了一个字符串的值和一个字符串的指针:

main.go
package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)
}

使用以下命令运行程序:

  • go run main.go

当您运行程序时,它将打印出变量的值,以及变量存储位置的地址(指针地址)。 内存地址是十六进制的数字,不适合人类阅读。 实际上,您可能永远不会输出一个内存地址来查看它。 我们展示给你们是为了说明问题。 因为每个程序在运行时都是在自己的内存空间中创建的,所以每次运行时指针的值都是不同的,并且与这里显示的输出不同:

Output
creature = shark pointer = 0xc0000721e0

我们定义的第一个变量名为 creature,并将其设置为一个值为 shark 的字符串。 然后我们创建了另一个名为 pointer 的变量。 这一次,我们将指针变量的值设置为生物变量的地址。 我们使用 & 符号将值的地址存储在变量中。 这意味着指针变量存储的是生物变量的地址,而不是实际值。

这就是为什么当我们打印出指针的值时,我们得到的值是0xc000721e0,这是生物变量当前存储在计算机内存中的地址。

如果要从指针变量中打印指向的变量的值,则需要取消引用该变量。 下面的代码使用 * 操作符解引用指针变量并检索其值:

main.go

package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)

    fmt.Println("*pointer =", *pointer)
}

如果你运行这段代码,你会看到如下输出:

Output
creature = shark pointer = 0xc000010200 *pointer = shark

我们添加的最后一行现在取消对指针变量的引用,并打印出存储在该地址的值。

如果你想修改存储在指针变量位置的值,你也可以使用解引用运算符:

main.go
package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)

    fmt.Println("*pointer =", *pointer)

    *pointer = "jellyfish"
    fmt.Println("*pointer =", *pointer)
}

运行以下代码查看输出:

Output
creature = shark pointer = 0xc000094040 *pointer = shark *pointer = jellyfish

我们通过在变量名称前面使用星号(*)来设置指针变量引用的值,然后提供一个新的 jellyfish 值。 如您所见,当我们打印解除引用值时,它现在被设置为 jellyfish。

你可能没有意识到,但是我们实际上也改变了生物变量的值。 这是因为指针变量实际上是指向生物变量的地址。 这意味着如果我们改变指针变量指向的值,我们也会改变生物变量的值。

main.go
package main

import "fmt"

func main() {
    var creature string = "shark"
    var pointer *string = &creature

    fmt.Println("creature =", creature)
    fmt.Println("pointer =", pointer)

    fmt.Println("*pointer =", *pointer)

    *pointer = "jellyfish"
    fmt.Println("*pointer =", *pointer)

    fmt.Println("creature =", creature)
}

输出如下:

Output
creature = shark pointer = 0xc000010200 *pointer = shark *pointer = jellyfish creature = jellyfish

尽管这段代码说明了指针是如何工作的,但这并不是在 Go 中使用指针的典型方式。 在定义函数参数和返回值时,或者在定义自定义类型的方法时使用它们时,使用它们更为常见。 让我们看看如何使用带有函数的指针来共享对变量的访问。

同样,请记住,我们打印指针的值来说明它是一个指针。 实际上,您不会使用指针的值,而是引用基础值来检索或更新该值。

函数指针接收器

在编写函数时,可以通过值或引用定义要传递的参数。 通过值传递意味着将该值的副本发送给函数,并且该函数中对该参数的任何更改只会影响该函数中的变量,而不会影响该变量从哪里传递。 但是,如果您通过引用传递,这意味着您传递了一个指向该参数的指针,那么您可以从函数内部更改该值,并且还可以更改传入的原始变量的值。 你可以在如何在围棋中定义和调用函数中了解更多关于如何定义函数的信息。

决定什么时候传递指针,而不是什么时候发送值,完全在于知道该值是否要更改。 如果不希望更改值,则将其作为值发送。 如果你希望你传递给变量的函数能够改变它,那么你可以将它作为一个指针传递。

为了看出区别,让我们首先看一个按值传入参数的函数:

main.go
package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

输出如下:

Output
1) {Species:shark} 2) {Species:jellyfish} 3) {Species:shark}

首先,我们创建了一个名为 Creature 的自定义类型。 它有一个名为 Species 的字段,这是一个字符串。 在 main 函数中,我们创建了一个新类型命名生物的实例,并将 Species 字段设置为 shark。 然后我们打印出该变量,以显示存储在该生物变量中的当前值。

接下来,我们调用 changeCreature 并传递一个生物变量的副本。

函数 changeCreature 被定义为接受一个名为 Creature 的参数,它是我们前面定义的 Creature 类型。 然后我们将 Species 字段的值更改为 jellyfish,并将其打印出来。 注意,在 changeCreature 函数中,Species 的值现在是 jellyfish,它打印出了2){ Species: jellyfish }。 这是因为我们被允许在我们的函数范围内更改值。

然而,当主函数的最后一行显示生物的价值时,物种的价值仍然是鲨鱼的。 值没有更改的原因是,我们按值传递了变量。 这意味着在内存中创建了该值的副本,并将其传递给 changeCreature 函数。 这允许我们有一个函数,它可以根据需要对传入的任何参数进行更改,但不会影响函数之外的任何变量。

接下来,让我们更改 changeCreature 函数,以便通过引用获取参数。 我们可以通过使用星号(*)操作符将生物类型改为指针来做到这一点。 我们现在不是传递一个生物,而是传递一个指向一个生物的指针。 在前面的例子中,生物是一个结构体,其物种值为鲨鱼。 * 生物是一个指针,而不是一个结构,所以它的值是一个内存位置,这就是我们传递给 changeCreature ()的值。

main.go
package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    changeCreature(&creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

运行这段代码可以看到以下输出:

Output
1) {Species:shark} 2) &{Species:jellyfish} 3) {Species:jellyfish}

请注意,现在当我们在 changeCreature 函数中将 Species 的值更改为 jellyfish 时,它也会更改 main 函数中定义的原始值。 这是因为我们通过引用传递了生物变量,它允许访问原始值,并且可以根据需要进行更改。

因此,如果希望函数能够更改值,则需要通过引用传递该值。 要通过引用传递,需要将指针传递给变量,而不是变量本身。

但是,有时您可能没有为指针定义实际值。 在这种情况下,程序中可能会出现恐慌。 让我们来看看这是如何发生的,以及如何对潜在的问题进行规划。

零指针

Go 中的所有变量都有一个零值。 即使对于指针来说也是如此。 如果将指针声明为类型,但没有赋值,则零值将为 nil。 Nil 表示变量的“ nothing has been initialized”。

在下面的程序中,我们定义了一个指向生物类型的指针,但是我们从来没有实例化一个生物的实际例子,并且把它的地址分配给生物指针变量。 该值将为 nil,我们不能引用任何将在生物类型上定义的字段或方法:

main.go
package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature *Creature

    fmt.Printf("1) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

输出如下:

Output
1) <nil> panic: runtime error: invalid memory address or nil pointer dereference [signal SIGSEGV: segmentation violation code=0x1 addr=0x8 pc=0x109ac86] goroutine 1 [running]: main.changeCreature(0x0) /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:18 +0x26 main.main() /Users/corylanou/projects/learn/src/github.com/gopherguides/learn/_training/digital-ocean/pointers/src/nil.go:13 +0x98 exit status 2

当我们运行这个程序时,它会打印出生物变量的值,而这个值是零。 然后我们调用 changeCreature 函数,当该函数尝试设置 Species 字段的值时,它会感到恐慌。 这是因为实际创建的变量没有实例。 正因为如此,程序没有实际存储值的位置,所以程序会感到慌乱。

在 Go 中很常见的一种情况是,如果你收到一个参数作为一个指针,在对它执行任何操作之前,你要检查它是否是 nil,以防止程序恐慌。

这是检查无的常用方法:

if someVariable == nil {
    // print an error or return from the method or fuction
}

实际上,您希望确保没有传递到函数或方法中的 nil 指针。 如果这样做,您可能只是想返回,或者返回一个错误,以显示向函数或方法传递了无效的参数。 下面的代码演示了如何检查 nil:

main.go
package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature *Creature

    fmt.Printf("1) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    if creature == nil {
        fmt.Println("creature is nil")
        return
    }

    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

我们在 changeCreature 中添加了一个检查,以查看生物参数的值是否为 nil。 如果是,我们打印出“生物为 nil” ,然后返回函数。 否则,我们继续并更改物种字段的值。 如果我们运行这个程序,我们现在会得到如下输出:

Output
1) <nil> creature is nil 3) <nil>

请注意,虽然我们仍然有一个 nil 值的生物变量,我们不再惊慌,因为我们正在检查该场景。

最后,如果我们创建一个生物类型的实例并将其分配给生物变量,程序现在将按照预期更改该值:

main.go
package main

import "fmt"

type Creature struct {
    Species string
}

func main() {
    var creature *Creature
    creature = &Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    changeCreature(creature)
    fmt.Printf("3) %+v\n", creature)
}

func changeCreature(creature *Creature) {
    if creature == nil {
        fmt.Println("creature is nil")
        return
    }

    creature.Species = "jellyfish"
    fmt.Printf("2) %+v\n", creature)
}

现在我们有了一个生物类型的实例,程序将运行,我们将得到以下预期输出:

Output
1) &{Species:shark} 2) &{Species:jellyfish} 3) &{Species:jellyfish}

当您使用指针时,程序可能会感到恐慌。 为了避免恐慌,在尝试访问在指针值上定义的任何字段或方法之前,应该检查指针值是否为 nil。

接下来,让我们看看使用指针和值如何影响类型上的定义方法。

方法指针接收器

在 go 中的接收器是在方法声明中定义的参数。 看看下面的代码:

type Creature struct {
    Species string
}

func (c Creature) String() string {
    return c.Species
}

这种方法的接收者是 c 生物。 它说明 c 的实例是生物类型,你将通过该实例变量引用该类型。

就像函数的行为根据是否将参数作为指针或值发送而不同一样,方法也有不同的行为。 最大的区别在于,如果您定义了一个带有值接收器的方法,那么您就不能对定义该方法的类型的实例进行更改。

有时候,您希望您的方法能够更新您正在使用的变量的实例。 为了实现这一点,你需要让接收者成为一个指针。

让我们为我们的生物类型添加一个 Reset 方法,将设置物种字段为空字符串:

main.go
package main

import "fmt"

type Creature struct {
    Species string
}

func (c Creature) Reset() {
    c.Species = ""
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    creature.Reset()
    fmt.Printf("2) %+v\n", creature)
}

如果我们运行这个程序,我们会得到如下输出:

Output
1) {Species:shark} 2) {Species:shark}

请注意,即使在 Reset 方法中,我们将 Species 的值设置为空字符串,当我们在 main 函数中打印出生物变量的值时,该值仍然设置为 shark。 这是因为我们定义了 Reset 方法有一个值接收器。 这意味着该方法只能访问该生物变量的一个副本。

如果我们想要在方法中修改生物变量的实例,我们需要定义它们有一个指针接收器:

main.go
package main

import "fmt"

type Creature struct {
    Species string
}

func (c *Creature) Reset() {
    c.Species = ""
}

func main() {
    var creature Creature = Creature{Species: "shark"}

    fmt.Printf("1) %+v\n", creature)
    creature.Reset()
    fmt.Printf("2) %+v\n", creature)
}

注意,我们现在在定义 Reset 方法时,在 Creature 类型的前面添加了一个星号(*)。 这意味着传递给 Reset 方法的 Creature 实例现在是一个指针,因此当我们进行更改时,它将影响该变量的原始实例。

Output
1) {Species:shark} 2) {Species:}

Reset 方法现在更改了 Species 字段的值。

总结

将函数或方法定义为按值传递或按引用传递将影响程序的哪些部分能够对其他部分进行更改。 控制什么时候可以更改该变量将允许您编写更健壮和可预测的软件。 现在您已经了解了指针,您可以看到它们是如何在接口中使用的。