瞭解 Go 中的指針


簡介

當你用 Go 編寫軟件時,你會編寫函數和方法。你將數據作爲 參數 傳遞給這些函數。

有時,函數會需要一個數據的本地拷貝,你希望原始數據保持不變。

例如,如果你是一家銀行,你有一個函數可以根據用戶選擇的儲蓄計劃來顯示他們的餘額變化,你不想在客戶選擇計劃之前改變他們的實際餘額,而只想用它來做計算。

這被稱爲 按值傳遞,因爲你是在向函數發送變量的值,而不是變量本身。

其他時候,你可能希望函數能夠改變原始變量中的數據。

例如,當銀行客戶向其賬戶存款時,你希望存款函數能夠訪問實際的餘額,而不是一個副本。在這種情況下,你不需要向函數發送實際數據, 而只需要告訴函數數據在內存中的位置。

一個叫做 指針 的數據類型持有數據的內存地址,但不是數據本身。內存地址告訴函數在哪裏可以找到數據,而不是數據的值。你可以把指針傳給函數而不是實際的數據,然後函數就可以在原地改變原始變量的值。

這被稱爲 通過引用傳遞,因爲變量的值並沒有傳遞給函數,而是傳遞了它指向的位置。

在這篇文章中,你將創建並使用指針來分享對一個變量的內存空間的訪問。

定義和使用指針

當你使用一個指向變量的指針時,有幾個不同的語法元素你需要了解。

第一個是與號(&)的使用。如果你在一個變量名稱前面加一個與號,你就說明你想獲得 地址,或者說是該變量的一個指針。

第二個語法元素是使用星號(*)或 引用 操作符。當你聲明一個指針變量時,你在變量名後面加上指針指向的變量類型,前面加一個*,像這樣:

var myPointer *int32 = &someint

這將創建 myPointer 作爲一個指向 int32 變量的指針,並以 someint 的地址初始化該指針。指針實際上並不包含一個 int32,而只是一個地址。

讓我們來看看一個指向 string 的指針。下面的代碼既聲明瞭一個字符串的值,又聲明瞭一個指向字符串的指針:

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

當你運行程序時,它將打印出變量的值,以及該變量的存儲地址(指針地址)。

內存地址是一個十六進制的數字,並不是爲了讓人看懂。

在實踐中,你可能永遠不會輸出內存地址來查看它。

我們給你看是爲了說明問題。因爲每個程序運行時都是在自己的內存空間中創建的,所以每次運行時指針的值都會不同,也會與下面顯示的輸出不同:

creature = shark
pointer = 0xc0000721e0

我們定義的第一個變量名爲 creature,並將其設置爲一個 string,其值爲 shark

然後我們創建了另一個名爲 pointer 的變量。這一次,我們將 pointer 變量的值設置爲 creature 變量的地址。我們通過使用與號(&)符號將一個值的地址存儲在一個變量中。

這意味着 pointer 變量存儲的是 creature 變量的 地址 ,而不是實際值。這就是爲什麼當我們打印出 pointer 的值時,我們收到的值是 0xc0000721e0 ,這是 creature 變量目前在計算機內存中的地址。

如果你想打印出 pointer 變量所指向的變量的值,你需要 解引用 該變量。

下面的代碼使用 * 操作符來解除對 pointer 變量的引用並檢索其值。

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

如果你運行這段代碼,你會看到以下輸出:

creature = shark
pointer = 0xc000010200
*pointer = shark

我們添加的最後一行現在解除了對 pointer 變量的引用,並打印出了存儲在該地址的值。 

如果你想修改存儲在 pointer 變量位置的值,你也可以使用解除引用操作:

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

運行這段代碼可以看到輸出:

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

我們通過在變量名稱前使用星號(*)來設置 pointer 變量所指的值,然後提供一個 jellyfish 的新值。

正如你所看到的,當我們打印解引用的值時,它現在被設置爲 jellyfish 。 

你可能沒有意識到,但實際上我們也改變了 creature 變量的值。

這是因爲 pointer 變量實際上是指向 creature 變量的地址。這意味着如果我們改變了 pointer 變量所指向的值,同時我們也會改變 creature 變量的值。

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

輸出看起來像這樣:

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

雖然這段代碼說明了指針的工作原理,但這並不是你在 Go 中使用指針的典型方式。

更常見的是在定義函數參數和返回值時使用它們,或者在定義自定義類型的方法時使用它們。

讓我們看看如何在函數中使用指針來共享對一個變量的訪問。同樣,請記住,我們正在打印 pointer 的值,是爲了說明它是一個指針。

在實踐中,你不會使用指針的值,除了引用底層的值來檢索或更新該值之外。

函數指針接收器

當你寫一個函數時,你可以定義參數,以 引用 的方式傳遞。

通過 傳遞意味着該值的副本被髮送到函數中,並且在該函數中對該參數的任何改變 在該函數中影響該變量,而不是從哪裏傳遞。

然而,如果你通過 引用 傳遞,意味着你傳遞了一個指向該參數的指針,你可以在函數中改變該值,也可以改變傳遞進來的原始變量的值。

你可以在我們的《如何在 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)
}

輸出看起來像這樣:

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

首先我們創建了一個名爲 Creature 的自定義類型。它有一個名爲 Species 的字段,它是一個字符串。在 main 函數中,我們創建了一個名爲Creature 的新類型實例,並將Species 字段設置爲shark

然後我們打印出變量,以顯示存儲在 creature 變量中的當前值。

接下來,我們調用 changeCreature,並傳入 creature 變量的副本。

changeCreature 被定義爲接受一個名爲 creature 的參數,並且它是我們之前定義的 Creature 類型的函數。

然後我們將Species 字段的值改爲 jellyfish 並打印出來。

注意在 changeCreature 函數中,Species 的值現在是 jellyfish,並且打印出 2) {Species:jellyfish}。這是因爲我們被允許在我們的函數範圍內改變這個值。

然而,當 main 函數的最後一行打印出 creature 的值時,Species 的值仍然是 shark

值沒有變化的原因是我們通過 傳遞變量。這意味着在內存中創建了一個值的副本,並傳遞給 changeCreature 函數。這允許我們有一個函數,可以根據需要對傳入的任何參數進行修改,但不會影響函數之外的任何變量。

接下來,讓我們改變 changeCreature 函數,使其通過 引用 接受一個參數。

我們可以通過使用星號(*)操作符將類型從 Creature 改爲指針來做到這一點。我們現在傳遞的不是一個 Creature,而是一個指向 Creature 的指針,或者是一個 *Creature

在前面的例子中,creature 是一個 struct,它的 Species 值爲 shark*creature 是一個指針,不是一個結構體,所以它的值是一個內存位置,這就是我們傳遞給 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)
}

運行這段代碼可以看到以下輸出:

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

注意,現在當我們在 changeCreature 函數中把 Species 的值改爲 jellyfish 時,它也改變了 main 函數中定義的原始值。

這是因爲我們通過 引用 傳遞了 creature 變量,它允許訪問內存裏的原始值並可以根據需要改變它。 

因此,如果你想讓一個函數能夠改變一個值,你需要通過引用來傳遞它。要通過引用傳遞,你就需要傳遞變量的指針,而不是變量本身。

然而,有時你可能沒有爲一個指針定義一個實際的值。在這些情況下,有可能在程序中出現 恐慌(點擊跳轉查看哦)

讓我們來看看這種情況是如何發生的,以及如何對這種潛在的問題進行規劃。

空指針

Go 中的所有變量都有一個零值(點擊跳轉查看哦)

即使對指針來說也是如此。如果你聲明瞭一個類型的指針,但是沒有賦值,那麼零值將是 nilnil 是一種表示變量 "沒有被初始化" 的方式。

在下面的程序中,我們定義了一個指向 Creature 類型的指針,但是我們從來沒有實例化過 Creature 的實際實例,也沒有將它的地址分配給 creature 指針變量。該值將是 nil,因此我們不能引用任何定義在 Creature 類型上的字段或方法:

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

輸出看起來像這樣:

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

當我們運行程序時,它打印出了 creature 變量的值,該值是 <nil>

然後我們調用 changeCreature 函數,當該函數試圖設置 Species 字段的值時,它 _panics _(恐慌) 了。

這是因爲實際上沒有創建 creature 變量的實例。正因爲如此,程序沒有地方可以實際存儲這個值,所以程序就恐慌了。

在 Go 中很常見的是,如果你以指針的形式接收一個參數,在對它進行任何操作之前,你要檢查它是否爲 nil,以防止程序恐慌。

這是檢查 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 中添加了一個檢查,看 creature 參數的值是否爲 nil。如果是,我們打印出 "creature is nil",並返回函數。否則,我們繼續並改變 Species 字段的值。

如果我們運行該程序,我們現在將得到以下輸出:

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

請注意,雖然我們仍然爲 creature 變量設置了 nil 值,但我們不再恐慌,因爲我們正在檢查這種情況。

最後,如果我們創建一個 Creature 類型的實例,並將其賦值給 creature 變量,程序現在將按照預期改變值:

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

現在我們有了一個 Creature 類型的實例,程序將運行,我們將得到以下預期輸出:

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字段設置爲一個空字符串:

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

如果我們運行該程序,我們將得到以下輸出:

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

注意到即使在 Reset 方法中我們將 Species 的值設置爲空字符串,當我們在 main 函數中打印出 creature 變量的值時,該值仍然被設置爲 shark

這是因爲我們定義的 Reset 方法有一個 接收器。這意味着該方法只能訪問 creature 變量的 副本

如果我們想在方法中修改 creature 變量的實例,我們需要將它們定義爲有一個 指針 接收器:

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 實例現在是一個指針,因此當我們進行修改時,將影響到該變量的原始實例。

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

現在 Reset 方法已經改變了 Species 字段的值。

總結

將一個函數或方法定義爲通過 或通過 引用,將影響你的程序的哪些部分能夠對其他部分進行修改。控制該變量何時能被改變,將使你能寫出更健壯和可預測的軟件。

現在你已經瞭解了指針,你也可以看到它們是如何在接口中使用的了。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RjLq4wUMvh5WMl8BcUmk0g