瞭解 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 中的所有變量都有一個零值(點擊跳轉查看哦)。
即使對指針來說也是如此。如果你聲明瞭一個類型的指針,但是沒有賦值,那麼零值將是 nil
。nil
是一種表示變量 "沒有被初始化" 的方式。
在下面的程序中,我們定義了一個指向 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