本篇翻譯自《Practical Go Lessons》 Chapter 15: Pointer type
1 你將在本章將學到什麼?
- 什麼是指標?
- 什麼時指標型別?
- 如何去建立並使用一個指標型別的變數。
- 指正型別變數的零值是什麼?
- 什麼是解除引用?
- slices, maps, 和 channels 有什麼特殊的地方?
2 涵蓋的技術概念
- 指標
- 記憶體地址
- 指標型別
- 解除引用
- 引用
3 什麼是指標?
指標是“是一個資料項,它儲存另外一個資料項的位置”。
在程式中,我們不斷地儲存和檢索資料。例如,字串、數字、複雜結構…。在物理層面,資料儲存在記憶體中的特定地址,而指標儲存的就是這些特定記憶體地址。
記住指標變數,就像其他變數一樣,它也有一個記憶體地址。
4 指標型別
Go 中的指標型別不止一種,每一種普通型別就對應一個指標型別。相應地,指標型別也限定了它自己只能指向對應型別的普通變數(地址)。
指標型別的語法為:
*BaseType
BaseType
指代的是任何普通型別。
我們來看一下例子:
*int
表示指向int
型別的指標*uint8
表示指向uint8
型別的指標
type User struct {
ID string
Username string
}
*User
表示指向User
型別的指標
5 如何去建立一個指標型別變數?
下面的語法可以建立:
var p *int
這裡我們建立了一個型別為 *int
的變數 p
。*int
是指標型別(基礎型別是 int
)。
讓我們來建立一個名為 answer
的整型變數。
var answer int = 42
現在我們給變數 p
分配一個值了:
p = &answer
使用 &
符號我們就能得到變 answer
的地址。來列印出這個地址~
fmt.Println(p)
// 0xc000012070
0xc000012070
是一個十六進位制數字,因為它的以 0x
為字首。記憶體地址通常是以十六進位制格式表示。你也可以使用二進位制(用 0 和 1)表示,但不易讀。
6 指標型別的零值
指標型別的零值都是 nil
,也就是說,一個沒有儲存地址的指標等於 nil
var q *int
fmt.Println(q == nil)
// true
7 解除引用
一個指標變數持有另一個變數的地址。如果你想通過指標去訪問地址背後的變數值該怎麼辦?你可以使用解除引用操作符 *
。
來舉個例子,我們定義一個結構體型別 Cart
:
type Cart struct {
ID string
Paid bool
}
然後我們建立一個 Cart
型別的變數 cart
,我們可以得到這個變數的地址,也可以通過地址找到這個變數:
- 使用
*
操作符,你可以通過地址找到變數值 - 使用
&
操作符,你可以得到變數的地址
7.1 空指標解引用:執行時 panic
每個 Go 程式設計師都會遇到這個 panic(報錯):
panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1091507]
為了更好地理解它,我們來複現一下:
package main
import "fmt"
func main() {
var myPointerVar *int
fmt.Println(*myPointerVar)
}
在程式裡,我們的定義了一個指標變數 myPointerVar
,這個變數的型別是 *int
(指向整型)。
然後我嘗試對它進行解引用,myPointerVar
變數持有一個尚未初始化的指標,因此該指標的值為 nil
。因為我們嘗試去尋找一個不存在的地址,程式將會報錯!我們嘗試找到空地址,而空地址在記憶體中不存在。
8 Maps 和 channels
Maps 和 channels 變數裡儲存了對內部結構的指標。因此,即便向一個函式或方法傳遞的 map 或 channel 不是指標型別,也開始對這個 map 或 channel 進行修改。讓我們看一個例子:
func addElement(cities map[string]string) {
cities["France"] = "Paris"
}
- 這個函式將一個 map 作為輸入
- 它向 map 中新增一項資料(key = "France", value = "Paris")
package main
import "log"
func main() {
cities := make(map[string]string)
addElement(cities)
log.Println(cities)
}
- 我們初始化一個名為
cities
的 map - 然後呼叫函式
addElement
- 程式列印出:
map[France:Paris]
我們將在專門的部分中更廣泛地介紹 channels 和 maps。
9 切片
9.1 切片定義
切片是相同型別元素的集合。在內部,切片是一個具有三個欄位的結構:
- length:長度
- capacity:容量
- pointer:執向內部陣列的指標
下面是一個關於切片EUcountries
的例子:
package main
import "log"
func main() {
EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
log.Println(EUcountries)
}
9.2 函式或方法將切片作為引數或接收器:小心
9.2.0.1 Example1: 向切片新增元素
package main
import "log"
func main() {
EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
addCountries(EUcountries)
log.Println(EUcountries)
}
func addCountries(countries []string) {
countries = append(countries, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
}
- 函式
addCountries
將一個字串型別切片作為引數 - 它通過內建函式
append
向切片新增字串來修改切片 - 它將缺失的歐盟國家附加到切片中
問題:依你看,程式的輸出將會是下面的哪個?
[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
[Austria Belgium Bulgaria]
答案:這個函式實際輸出:
[Austria Belgium Bulgaria]
9.2.0.2 解釋
- 這個函式將
[]string
型別元素作為引數 - 當函式被呼叫時,Go 會將切片
EUcountries
拷貝一份傳進去 - 函式將得到一個拷貝的切片資料:
- 長度
- 容量
- 指向底層資料的指標
- 在函式內部,缺失的國家被新增了進去
- 切片的長度會增加
- 執行時將分配一個新的內部陣列
讓我們在函式中新增一個日誌來視覺化它:
func addCountries(countries []string) {
countries = append(countries, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
log.Println(countries)
}
日誌列印出:
[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
- 這裡的改變只會影響拷貝的版本
9.2.0.3 Example2:更新元素
package main
import (
"log"
"strings"
)
func main() {
EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
upper(EUcountries)
log.Println(EUcountries)
}
func upper(countries []string) {
for k, _ := range countries {
countries[k] = strings.ToUpper(countries[k])
}
}
- 我們新增新函式
upper
,它將把一個字串切片的每個元素都轉換成大寫
問題:依你看,程式將傳輸下面哪個?
[AUSTRIA BELGIUM BULGARIA]
[Austria Belgium Bulgaria]
答案:這個函式將返回:
[AUSTRIA BELGIUM BULGARIA]
9.2.0.4 解釋
- 函式
upper
獲取切片 EUcountries 的副本(和上面一樣) - 在函式內部,我們更改切片元素的值
countries[k] = strings.ToUpper(countries[k])
- 切片副本仍然有對底層陣列的引用
- 我們可以修改!
- .. 但只有已經在切片中的切片元素。
9.2.0.5 結論
- 當你將切片傳遞給函式時,它會獲取切片的副本。
- 這並不意味著你不能修改切片。
- 你只可以修改切片中已經存在的元素。
9.3 函式或方法將切片指標作為引數或接收器
如果使用切片指標,你就可以在函式中修改這個切片了:
package main
import (
"log"
)
func main() {
EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
addCountries2(&EUcountries)
log.Println(EUcountries)
}
func addCountries2(countriesPtr *[]string) {
*countriesPtr = append(*countriesPtr, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
}
這個程式將輸出:
[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
- 函式
addCountries2
將字串切片的指標([]string
)作為引數 - 函式
append
呼叫時的第一個引數是*countriesPtr
(即我們通過指標countriesPtr
去找到原值) append
的第二個引數沒有改變- 函式
addCountries2
的結果會影響到外部的變數
10 指向結構體的指標
有一個快捷方式可以讓你直接修改 struct 型別的變數而無需使用*
運算子:
type Item struct {
SKU string
Quantity int
}
type Cart struct {
ID string
CreatedDate time.Time
Items Item
}
cart := Cart{
ID: "115552221",
CreatedDate: time.Now(),
}
cartPtr := &cart
cartPtr.Items = []Item{
{SKU: "154550", Quantity: 12},
{SKU: "DTY8755", Quantity: 1},
}
log.Println(cart.Items)
// [{154550 12} {DTY8755 1}]
cart
是一個Cart
型別變數cartPtr := &cart
會獲取變數 cart 的地址然後將其儲存到cartPtr
中- 使用變數
cartPtr
,我們可以直接修改變數cart
的Item
欄位 - 這是因為執行時自動通過結構體指標找到了原值進行了修改,以下是等價的寫法
(*carPtr).Items = []Item{
{SKU: "154550", Quantity: 12},
{SKU: "DTY8755", Quantity: 1},
}
(這也有效,但更冗長)
11 使用指標作為方法的接收器
指標通常用作方法的接收器,讓我們以 Cat
型別為例:
type Cat struct {
Color string
Age uint8
Name string
}
你可以定義一個方法,使用指向 Cat
的指標作為方法的接收器(*Cat
):
func (cat *Cat) Meow(){
fmt.Println("Meooooow")
}
Meow
方法沒有做任何有實際意義的事嗎;它只是列印了字串"Meooooow"
。我們沒有修改比變數的值。我們來看另一個方法,它修改了 cat 的 Name
:
func (cat *Cat) Rename(newName string){
cat.Name = newName
}
此方法將更改貓的名稱。通過指標,我們修改了 Cat 結構體的一個欄位。
當然,如果你不想使用指標作為接收器,你也可以:
func (cat Cat) RenameV2(newName string){
cat.Name = newName
}
在這個例子中,變數 cat
是一個副本。接收器被命名為“值接收器”。因此,你對 cat 變數所做的任何修改都將在 cat 副本上完成:
package main
import "fmt"
type Cat struct {
Color string
Age uint8
Name string
}
func (cat *Cat) Meow() {
fmt.Println("Meooooow")
}
func (cat *Cat) Rename(newName string) {
cat.Name = newName
}
func (cat Cat) RenameV2(newName string) {
cat.Name = newName
}
func main() {
cat := Cat{Color: "blue", Age: 8, Name: "Milow"}
cat.Rename("Bob")
fmt.Println(cat.Name)
// Bob
cat.RenameV2("Ben")
fmt.Println(cat.Name)
// Bob
}
在主函式的第一行,我們建立了一個 Cat
型別的變數 cat,它的 Name 是 "Millow"
。
當我們呼叫具有值接收器的 RenameV2
方法時,函式外部變數 cat 的 Name 沒有發生改變。
當我們呼叫 Rename
方法時,cat 的 Name 欄位值會發生變化。
11.1 何時使用指標接收器,何時使用值接收器
- 以下情況使用指標接收器:
- 你的結構體很大(如果使用值接收器,Go 會複製它)
- 你想修改接收器(例如,你想更改結構變數的名稱欄位)
- 你的結構包含一個同步原語(如sync.Mutex)欄位。如果你使用值接收器,它還會複製互斥鎖,使其無用並導致同步錯誤。
- 當接收器是一個 map、func、chan、slice、string 或 interface值時(因為在內部它已經是一個指標)
- 當你的接收器是持有指標時
12 隨堂測試
12.1 問題
- 如何去表示一個持有指向
Product
指標的變數? - 指標型別的零值是多少?
- "解引用(dereferencing)" 是什麼意思?
- 如何解引用一個指標?
- 填空: ____ 在內部是一個指向 ____ 的指標。
- 判斷正誤:當我想函式中修改 map 時,我的函式需要接收一個指向 map 的指標作為引數,我還需要返回修改後的 map?
12.2 答案
- 如何去表示一個持有指向
Product
指標的變數?
*Product
- 指標型別的零值是多少?
nil - "解引用(dereferencing)" 是什麼意思?
- 指標是指向儲存資料的記憶體位置的地址。
- 當我們解引用一個指標時,我們可以訪問儲存在該地址的記憶體中的資料。
- 如何解引用一個指標?
使用解引用操作符*
- 填空: ____ 在內部是一個指向 ____ 的指標。
slice 在內部是一個指向 array 的指標。 - 判斷正誤:當我想函式中修改 map 時,我的函式需要接收一個指向 map 的指標作為引數,我還需要返回修改後的 map
錯, 函式中只要接收一個 map 型別引數就行,也不需要返回更改後的map,因為 map 變數內部儲存了指向底層資料的指標
關鍵要點
- 指標是指向資料的地址
- 型別
*T
表示所有指向T
型別變數的指標集合 - 建立指標變數,可以使用運算子
&
。它將獲取一個變數的地址
userId := 12546584
p := &userId
`userId` 是 `int` 型別的變數
`p` 是 `*int` 型別變數
`*int` 表示所有指向 `int` 型別變數的指標
- 具有指標型別的引數/接收器的函式可以修改指標指向的值。
- map 和 channel 是“引用型別”
- 接收 map 或 channel 的函式/方法可以修改內部儲存在這兩個資料結構中的值(無需傳遞指向 map 的指標或指向 channel 的指標)
- 切片在內部儲存對陣列的引用;任何接收切片的函式/方法都可以修改切片元素。
- 當你想在函式中修改切片長度和容量時,你應該向該函式傳遞一個指向切片的指標 (
*[]string
) - 解引用允許你訪問和修改儲存在指標地址處的值。
- 要對指標進行解引用操作,請使用運算子
*
userId := 12546584
p := &userId
*p = 4
log.Println(userId)
p
是一個指標
- 我們使用
*p
來對指標p
進行解引用- 我們用指令
*p = 4
修改userId
的值- 在程式碼片段的末尾,userId 的值為 4(不再是 12546584)
- 當你有一個指向結構的指標時,你可以直接使用你的指標變數訪問一個欄位(不需要使用解引用運算子)
- 例子:
type Cart struct {
ID string
}
var cart Cart
cartPtr := &cart
- 不需要這樣寫:
(*cartPtr).ID = "1234"
- 你可直接這樣寫:
cartPtr.Items = "1234"
- 變數
cart
就會被修改