「每周譯Go」了解 Go 中的指針
目錄
- 在 Go 中導(dǎo)入包
- 理解 Go 中包的可見性
- 如何在 Go 中編寫條件語句
- 如何在 Go 中編寫 Switch 語句
- 如何在 Go 中構(gòu)造 for 循環(huán)
- 在循環(huán)中使用 Break 和 Continue
- 如何在 Go 中定義并調(diào)用函數(shù)
- 如何在 Go 中使用可變參數(shù)函數(shù)
- 了解 Go 中的 defer
- 了解 Go 中的 init
- 用構(gòu)建標(biāo)簽定制 Go 二進(jìn)制文件
- 了解 Go 中的指針
- 在 Go 中定義結(jié)構(gòu)體
- 在 Go 中定義方法
- 如何構(gòu)建和安裝 Go 程序
- 如何在 Go 中使用結(jié)構(gòu)體標(biāo)簽
- 如何在 Go 使用 interface
- 在不同的操作系統(tǒng)和架構(gòu)編譯 Go 應(yīng)用
- 用 ldflags 設(shè)置 Go 應(yīng)用程序的版本信息
- 在 Go 里面如何使用 Flag 包
了解 Go 中的指針
簡介
當(dāng)你用 Go 編寫軟件時,你會編寫函數(shù)和方法。你將數(shù)據(jù)作為 參數(shù) 傳遞給這些函數(shù)。
有時,函數(shù)會需要一個數(shù)據(jù)的本地拷貝,你希望原始數(shù)據(jù)保持不變。
例如,如果你是一家銀行,你有一個函數(shù)可以根據(jù)用戶選擇的儲蓄計(jì)劃來顯示他們的余額變化,你不想在客戶選擇計(jì)劃之前改變他們的實(shí)際余額,而只想用它來做計(jì)算。
這被稱為 按值傳遞,因?yàn)槟闶窃谙蚝瘮?shù)發(fā)送變量的值,而不是變量本身。
其他時候,你可能希望函數(shù)能夠改變原始變量中的數(shù)據(jù)。
例如,當(dāng)銀行客戶向其賬戶存款時,你希望存款函數(shù)能夠訪問實(shí)際的余額,而不是一個副本。在這種情況下,你不需要向函數(shù)發(fā)送實(shí)際數(shù)據(jù), 而只需要告訴函數(shù)數(shù)據(jù)在內(nèi)存中的位置。
一個叫做 指針 的數(shù)據(jù)類型持有數(shù)據(jù)的內(nèi)存地址,但不是數(shù)據(jù)本身。內(nèi)存地址告訴函數(shù)在哪里可以找到數(shù)據(jù),而不是數(shù)據(jù)的值。你可以把指針傳給函數(shù)而不是實(shí)際的數(shù)據(jù),然后函數(shù)就可以在原地改變原始變量的值。
這被稱為 通過引用傳遞,因?yàn)樽兞康闹挡]有傳遞給函數(shù),而是傳遞了它指向的位置。
在這篇文章中,你將創(chuàng)建并使用指針來分享對一個變量的內(nèi)存空間的訪問。
定義和使用指針
當(dāng)你使用一個指向變量的指針時,有幾個不同的語法元素你需要了解。
第一個是與號(&)的使用。如果你在一個變量名稱前面加一個與號,你就說明你想獲得 地址,或者說是該變量的一個指針。
第二個語法元素是使用星號(*)或 引用 操作符。當(dāng)你聲明一個指針變量時,你在變量名后面加上指針指向的變量類型,前面加一個*,像這樣:
var?myPointer?*int32?=?&someint
這將創(chuàng)建 myPointer 作為一個指向 int32 變量的指針,并以 someint 的地址初始化該指針。指針實(shí)際上并不包含一個 int32,而只是一個地址。
讓我們來看看一個指向 string 的指針。下面的代碼既聲明了一個字符串的值,又聲明了一個指向字符串的指針:
package?main
import?"fmt"
func?main()?{
?var?creature?string?=?"shark"
?var?pointer?*string?=?&creature
?fmt.Println("creature?=",?creature)
?fmt.Println("pointer?=",?pointer)
}
用以下命令運(yùn)行該程序:
go?run?main.go
當(dāng)你運(yùn)行程序時,它將打印出變量的值,以及該變量的存儲地址(指針地址)。
內(nèi)存地址是一個十六進(jìn)制的數(shù)字,并不是為了讓人看懂。
在實(shí)踐中,你可能永遠(yuǎn)不會輸出內(nèi)存地址來查看它。
我們給你看是為了說明問題。因?yàn)槊總€程序運(yùn)行時都是在自己的內(nèi)存空間中創(chuàng)建的,所以每次運(yùn)行時指針的值都會不同,也會與下面顯示的輸出不同:
creature?=?shark
pointer?=?0xc0000721e0
我們定義的第一個變量名為 creature,并將其設(shè)置為一個 string,其值為 shark 。
然后我們創(chuàng)建了另一個名為 pointer 的變量。這一次,我們將 pointer 變量的值設(shè)置為 creature 變量的地址。我們通過使用與號(&)符號將一個值的地址存儲在一個變量中。
這意味著 pointer 變量存儲的是 creature 變量的 地址 ,而不是實(shí)際值。這就是為什么當(dāng)我們打印出 pointer 的值時,我們收到的值是 0xc0000721e0 ,這是 creature 變量目前在計(jì)算機(jī)內(nèi)存中的地址。
如果你想打印出 pointer 變量所指向的變量的值,你需要 解引用 該變量。
下面的代碼使用 * 操作符來解除對 pointer 變量的引用并檢索其值。
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)
}
如果你運(yùn)行這段代碼,你會看到以下輸出:
creature?=?shark
pointer?=?0xc000010200
*pointer?=?shark
我們添加的最后一行現(xiàn)在解除了對 pointer 變量的引用,并打印出了存儲在該地址的值。?
如果你想修改存儲在 pointer 變量位置的值,你也可以使用解除引用操作:
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)
}
運(yùn)行這段代碼可以看到輸出:
creature?=?shark
pointer?=?0xc000094040
*pointer?=?shark
*pointer?=?jellyfish
我們通過在變量名稱前使用星號(*)來設(shè)置 pointer 變量所指的值,然后提供一個 jellyfish 的新值。
正如你所看到的,當(dāng)我們打印解引用的值時,它現(xiàn)在被設(shè)置為 jellyfish 。?
你可能沒有意識到,但實(shí)際上我們也改變了 creature 變量的值。
這是因?yàn)?pointer 變量實(shí)際上是指向 creature 變量的地址。這意味著如果我們改變了 pointer 變量所指向的值,同時我們也會改變 creature 變量的值。
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 中使用指針的典型方式。
更常見的是在定義函數(shù)參數(shù)和返回值時使用它們,或者在定義自定義類型的方法時使用它們。
讓我們看看如何在函數(shù)中使用指針來共享對一個變量的訪問。同樣,請記住,我們正在打印 pointer 的值,是為了說明它是一個指針。
在實(shí)踐中,你不會使用指針的值,除了引用底層的值來檢索或更新該值之外。
函數(shù)指針接收器
當(dāng)你寫一個函數(shù)時,你可以定義參數(shù),以 值 或 引用 的方式傳遞。
通過 值 傳遞意味著該值的副本被發(fā)送到函數(shù)中,并且在該函數(shù)中對該參數(shù)的任何改變 只 在該函數(shù)中影響該變量,而不是從哪里傳遞。
然而,如果你通過 引用 傳遞,意味著你傳遞了一個指向該參數(shù)的指針,你可以在函數(shù)中改變該值,也可以改變傳遞進(jìn)來的原始變量的值。
你可以在我們的《如何在 Go 中定義和調(diào)用函數(shù)》(點(diǎn)擊跳轉(zhuǎn)查看哦)中閱讀更多關(guān)于如何定義函數(shù)的信息。
什么時候傳遞一個指針,什么時候發(fā)送一個值,都取決于你是否希望這個值發(fā)生變化。
如果你不希望數(shù)值改變,就把它作為一個值來發(fā)送。如果你希望你傳遞給你的變量的函數(shù)能夠改變它,那么你就把它作為一個指針傳遞。
為了看到區(qū)別,讓我們先看看一個通過 值 傳遞參數(shù)的函數(shù):
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}
首先我們創(chuàng)建了一個名為 Creature 的自定義類型。它有一個名為 Species 的字段,它是一個字符串。在 main 函數(shù)中,我們創(chuàng)建了一個名為Creature 的新類型實(shí)例,并將Species 字段設(shè)置為shark 。
然后我們打印出變量,以顯示存儲在 creature 變量中的當(dāng)前值。
接下來,我們調(diào)用 changeCreature,并傳入 creature 變量的副本。
changeCreature 被定義為接受一個名為 creature 的參數(shù),并且它是我們之前定義的 Creature 類型的函數(shù)。
然后我們將Species 字段的值改為 jellyfish 并打印出來。
注意在 changeCreature 函數(shù)中,Species 的值現(xiàn)在是 jellyfish,并且打印出 2) {Species:jellyfish}。這是因?yàn)槲覀儽辉试S在我們的函數(shù)范圍內(nèi)改變這個值。
然而,當(dāng) main 函數(shù)的最后一行打印出 creature 的值時,Species 的值仍然是 shark 。
值沒有變化的原因是我們通過 值 傳遞變量。這意味著在內(nèi)存中創(chuàng)建了一個值的副本,并傳遞給 changeCreature 函數(shù)。這允許我們有一個函數(shù),可以根據(jù)需要對傳入的任何參數(shù)進(jìn)行修改,但不會影響函數(shù)之外的任何變量。
接下來,讓我們改變 changeCreature 函數(shù),使其通過 引用 接受一個參數(shù)。
我們可以通過使用星號(*)操作符將類型從 Creature 改為指針來做到這一點(diǎn)。我們現(xiàn)在傳遞的不是一個 Creature,而是一個指向 Creature 的指針,或者是一個 *Creature。
在前面的例子中,creature 是一個 struct,它的 Species 值為 shark。*creature 是一個指針,不是一個結(jié)構(gòu)體,所以它的值是一個內(nèi)存位置,這就是我們傳遞給 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)
}
運(yùn)行這段代碼可以看到以下輸出:
1)?{Species:shark}
2)?&{Species:jellyfish}
3)?{Species:jellyfish}
注意,現(xiàn)在當(dāng)我們在 changeCreature 函數(shù)中把 Species 的值改為 jellyfish 時,它也改變了 main 函數(shù)中定義的原始值。
這是因?yàn)槲覀兺ㄟ^ 引用 傳遞了 creature 變量,它允許訪問內(nèi)存里的原始值并可以根據(jù)需要改變它。?
因此,如果你想讓一個函數(shù)能夠改變一個值,你需要通過引用來傳遞它。要通過引用傳遞,你就需要傳遞變量的指針,而不是變量本身。
然而,有時你可能沒有為一個指針定義一個實(shí)際的值。在這些情況下,有可能在程序中出現(xiàn) 恐慌(點(diǎn)擊跳轉(zhuǎn)查看哦)。
讓我們來看看這種情況是如何發(fā)生的,以及如何對這種潛在的問題進(jìn)行規(guī)劃。
空指針
Go 中的所有變量都有一個零值(點(diǎn)擊跳轉(zhuǎn)查看哦)。
即使對指針來說也是如此。如果你聲明了一個類型的指針,但是沒有賦值,那么零值將是 nil。nil 是一種表示變量 "沒有被初始化" 的方式。
在下面的程序中,我們定義了一個指向 Creature 類型的指針,但是我們從來沒有實(shí)例化過 Creature 的實(shí)際實(shí)例,也沒有將它的地址分配給 creature 指針變量。該值將是 nil,因此我們不能引用任何定義在 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)
}
輸出看起來像這樣:
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
當(dāng)我們運(yùn)行程序時,它打印出了 creature 變量的值,該值是 <nil>。
然后我們調(diào)用 changeCreature 函數(shù),當(dāng)該函數(shù)試圖設(shè)置 Species 字段的值時,它 panics?(恐慌) 了。
這是因?yàn)閷?shí)際上沒有創(chuàng)建 creature 變量的實(shí)例。正因?yàn)槿绱?,程序沒有地方可以實(shí)際存儲這個值,所以程序就恐慌了。
在 Go 中很常見的是,如果你以指針的形式接收一個參數(shù),在對它進(jìn)行任何操作之前,你要檢查它是否為 nil,以防止程序恐慌。
這是檢查 nil 的一種常見方法:
if?someVariable?==?nil?{
?//?print?an?error?or?return?from?the?method?or?fuction
}
實(shí)際上,你想確保你沒有一個 nil 指針被傳入你的函數(shù)或方法。
如果有的話,你可能只想返回,或者返回一個錯誤,以表明一個無效的參數(shù)被傳遞到函數(shù)或方法中。
下面的代碼演示了對 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 中添加了一個檢查,看 creature 參數(shù)的值是否為 nil。如果是,我們打印出 "creature is nil",并返回函數(shù)。否則,我們繼續(xù)并改變 Species 字段的值。
如果我們運(yùn)行該程序,我們現(xiàn)在將得到以下輸出:
1)?<nil>
creature?is?nil
3)?<nil>
請注意,雖然我們?nèi)匀粸?creature 變量設(shè)置了 nil 值,但我們不再恐慌,因?yàn)槲覀冋跈z查這種情況。
最后,如果我們創(chuàng)建一個 Creature 類型的實(shí)例,并將其賦值給 creature 變量,程序現(xiàn)在將按照預(yù)期改變值:
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)
}
現(xiàn)在我們有了一個 Creature 類型的實(shí)例,程序?qū)⑦\(yùn)行,我們將得到以下預(yù)期輸出:
1)?&{Species:shark}
2)?&{Species:jellyfish}
3)?&{Species:jellyfish}
當(dāng)你在使用指針時,程序有可能會出現(xiàn)恐慌。為了避免恐慌,你應(yīng)該在試圖訪問任何字段或定義在其上的方法之前,檢查一個指針值是否為 nil。
接下來,讓我們看看使用指針和值是如何影響在一個類型上定義方法的。
方法指針接收器
Go 中的 接收器 是指在方法聲明中定義的參數(shù)。看一下下面的代碼:
type?Creature?struct?{
?Species?string
}
func?(c?Creature)?String()?string?{
?return?c.Species
}
這個方法的接收器是 c Creature。它說明 c 的實(shí)例屬于 Creature 類型,你將通過該實(shí)例變量引用該類型。
方法跟函數(shù)一樣,也是根據(jù)你送入的參數(shù)是指針還是值而有不同的行為。
最大的區(qū)別是,如果你用一個值接收器定義一個方法,你就不能對該方法所定義的那個類型的實(shí)例進(jìn)行修改。
有的時候,你希望你的方法能夠更新你所使用的變量的實(shí)例。為了實(shí)現(xiàn)這一點(diǎn),你會想讓接收器成為一個指針。
讓我們給我們的 Creature 類型添加一個 Reset 方法,將 Species字段設(shè)置為一個空字符串:
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)
}
如果我們運(yùn)行該程序,我們將得到以下輸出:
1)?{Species:shark}
2)?{Species:shark}
注意到即使在 Reset 方法中我們將 Species 的值設(shè)置為空字符串,當(dāng)我們在 main 函數(shù)中打印出 creature 變量的值時,該值仍然被設(shè)置為 shark 。
這是因?yàn)槲覀兌x的 Reset 方法有一個 值 接收器。這意味著該方法只能訪問 creature 變量的 副本。
如果我們想在方法中修改 creature 變量的實(shí)例,我們需要將它們定義為有一個 指針 接收器:
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)
}
注意,我們現(xiàn)在在定義 Reset 方法時,在 Creature 類型前面添加了一個星號(*)。這意味著傳遞給 Reset 方法的 Creature 實(shí)例現(xiàn)在是一個指針,因此當(dāng)我們進(jìn)行修改時,將影響到該變量的原始實(shí)例。
1)?{Species:shark}
2)?{Species:}
現(xiàn)在 Reset 方法已經(jīng)改變了 Species 字段的值。
總結(jié)
將一個函數(shù)或方法定義為通過 值 或通過 引用,將影響你的程序的哪些部分能夠?qū)ζ渌糠诌M(jìn)行修改??刂圃撟兞亢螘r能被改變,將使你能寫出更健壯和可預(yù)測的軟件。
現(xiàn)在你已經(jīng)了解了指針,你也可以看到它們是如何在接口中使用的了。
往期推薦
「每周譯Go」用構(gòu)建標(biāo)簽定制Go二進(jìn)制文件
Go 1.20新變化!第一部分:語言特性
想要了解Go更多內(nèi)容,歡迎掃描下方??關(guān)注公眾號, 回復(fù)關(guān)鍵詞 [實(shí)戰(zhàn)群]? ?,就有機(jī)會進(jìn)群和我們進(jìn)行交流
分享、在看與點(diǎn)贊Go?



