跳转到主要内容

介绍


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

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

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

定义和使用指针


当您使用指向变量的指针时,您需要了解几个不同的语法元素。第一个是与号 (&) 的使用。如果您在变量名前放置一个 & 符号,则表示您想要获取地址或指向该变量的指针。第二个语法元素是使用星号 (*) 或取消引用运算符。当你声明一个指针变量时,你在变量名后面加上指针指向的变量的类型,以 * 为前缀,如下所示:

var myPointer *int32 = &someint


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

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

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


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

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

如果要打印出指针变量指向的变量的值,则需要取消对该变量的引用。以下代码使用 * 运算符取消引用指针变量并检索其值:

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


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

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

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。

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

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 中使用指针的典型方式。更常见的是在定义函数参数和返回值时使用它们,或者在自定义类型上定义方法时使用它们。让我们看看如何将指针与函数一起使用来共享对变量的访问。

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

函数指针接收器


当你编写一个函数时,你可以定义通过值或引用传递的参数。按值传递意味着将该值的副本发送到函数,并且对该函数中该参数的任何更改仅影响该函数中的该变量,而不影响它的传递位置。但是,如果通过引用传递,即传递指向该参数的指针,则可以从函数内部更改值,也可以更改传入的原始变量的值。您可以阅读有关如何定义函数的更多信息在我们的如何在 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 的字段,它是一个字符串。在主函数中,我们创建了一个名为 bio 的新类型的实例,并将 Species 字段设置为 Shark。然后我们打印出变量以显示存储在生物变量中的当前值。

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

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

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

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

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 时,它也会更改主函数中定义的原始值。这是因为我们通过引用传递了生物变量,它允许访问原始值并可以根据需要更改它。

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

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

零指针


Go 中的所有变量都具有零值。即使对于指针也是如此。如果你声明了一个指向类型的指针,但没有赋值,那么零值将是 nil。 nil 是一种表示变量“没有被初始化”的方式。

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

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

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

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

这是检查 nil 的常用方法:

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

实际上,您希望确保没有将 nil 指针传递给您的函数或方法。如果这样做,您可能只想返回或返回错误以表明向函数或方法传递了无效参数。以下代码演示了对 nil 的检查:

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。 如果是,我们打印出“creature is nil”,然后从函数中返回。 否则,我们继续并更改 Species 字段的值。 如果我们运行程序,我们现在将得到以下输出:

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


请注意,虽然 bio 变量的值仍然为零,但我们不再恐慌,因为我们正在检查这种情况。

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

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)
}

现在我们有了一个 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 Creature。它说明 c 的实例是 Creature 类型,您将通过该实例变量引用该类型。

就像函数的行为不同,取决于您将参数作为指针还是值发送,方法也有不同的行为。最大的区别在于,如果您使用值接收器定义方法,则无法更改定义该方法的该类型的实例。

有时您希望您的方法能够更新您正在使用的变量的实例。为此,您需要将接收器设为指针。

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

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 的值设置为空字符串,当我们在主函数中打印出生物变量的值时,该值仍然设置为鲨鱼。 这是因为我们定义了 Reset 方法有一个值接收器。 这意味着该方法只能访问生物变量的副本。

如果我们希望能够在方法中修改生物变量的实例,我们需要将它们定义为具有指针接收器:

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 字段的值。

结论


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

文章链接