和許多其他語言一樣,Go 也有指標。 如果你剛開始接觸指標,可能會覺得它們有點神祕,但一旦習慣之後,就會發現它們其實相當直觀。 指標是 Go 很關鍵的一部分,所以請花點時間好好理解它們吧。
在深入細節之前,先了解指標的用途很值得。 指標是一種和程式其他部分共享記憶體的方式,它在兩個主要情況下很有用:
假設我們有一個普通的整數變數 a:
var a int
當我們宣告一個變數時,Go 必須在記憶體中找到一個地方來儲存它的值。 這對我們來說大部分都被抽象化了:當我們需要取出那塊記憶體裡儲存的值時,只要用變數名稱來取用就好。
例如,當我們寫 a + 2 時,實際上就是取出變數 a 所對應的記憶體中儲存的值,並加上 2。
同樣地,當我們需要改變 a 那塊記憶體裡的值時,可以用變數名稱來做指定:
a = 3
與 a 相關的那塊記憶體現在會儲存值 3。
變數讓我們能取用記憶體中的值,不過有時知道變數所指向的記憶體位址會很有用。指標就是用來儲存這些值的記憶體位址。 要宣告一個指標型別的變數,只要在底層型別前面加上星號:
var p *int // 'p' contains the memory address of an integer
這裡我們宣告了一個型別為「指向 int 的指標」(*int)的變數 p。
這表示 p 會儲存某個整數的記憶體位址。
指標的零值是 nil,因為 nil 指標不儲存任何記憶體位址。
要找出某個變數的值的記憶體位址,可以使用 & 運算子。
例如,如果我們想找出變數 a 的記憶體位址並存進指標 p,可以這樣做:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
當我們有一個指標時,可能會想知道該指標所代表的記憶體位址中儲存的值。
我們可以用 * 運算子來做到這件事:
var a int
a = 2
var p *int
p = &a // the variable 'p' contains the memory address of 'a'
var b int
b = *p // b == 2
*p 這個操作會取出存在 p 裡的記憶體位址中所儲存的值。
這個操作通常稱為「解引用」。
我們也可以用解引用運算子,把新的值指定給指標所參考的記憶體位址:
var a int // declare int variable 'a'
a = 2 // assign 'a' the value of 2
var pa *int
pa = &a // 'pa' now contains to the memory address of 'a'
*pa = *pa + 2 // increment by 2 the value at memory address 'pa'
fmt.Println(a) // Output: 4
// 'a' will have the new value that was changed via the pointer!
指定給 *pa 會改變 pa 所持有的記憶體位址中儲存的值。
由於 pa 持有 a 的記憶體位址,所以指定給 *pa 實際上就是在改變 a 的值!
不過要提醒一點:解引用之前,一定要先確認指標不是 nil。
解引用 nil 指標會讓程式在執行時崩潰!
var p *int // p is nil initially
fmt.Println(*p)
// panic: runtime error: invalid memory address or nil pointer dereference
到目前為止,我們只看過指向基本型別值的指標。 我們也可以建立指向結構體的指標:
type Person struct {
Name string
Age int
}
var peter Person
peter = Person{Name: "Peter", Age: 22}
var p *Person
p = &peter
我們也可以建立一個新的 Person,並立刻儲存指向它的指標:
var p *Person
p = &Person{Name: "Peter", Age: 22}
當我們有指向結構體的指標時,不需要先解引用就能存取其中一個欄位:
var p *Person
p = &Person{Name: "Peter", Age: 22}
fmt.Println(p.Name) // Output: "Peter"
// Go automatically dereferences 'p' to allow
// access to the 'Name' field
切片和映射是特殊的型別,因為它們的實作本身就已經帶有指標。 這表示大多數情況下,我們不需要為這些型別建立指標,就能共享它們的值的記憶體位址。 想像我們有一個函式,會把映射中某個鍵的值加一:
func incrementPeterAge(m map[string]int) {
m["Peter"] += 1
}
如果我們建立一個映射並呼叫這個函式,函式對映射所做的變更會在函式結束後保留下來。 這種行為和我們使用指標時很像,但請注意,在這個例子裡我們並沒有用到任何取參考/解引用,也沒有用到任何指標語法:
ages := map[string]int{
"Peter": 21
}
incrementPeterAge(ages)
fmt.Println(ages)
// Output: map[Peter:22]
// The changes the function 'incrementPeterAge' made to the map are visible after the function ends!
修改切片中既有的元素時也是一樣。
不過,像 append 這樣會回傳新切片的操作是特例,它可能不會修改函式外面的切片。
這是因為切片內部運作方式的關係,但這屬於比較進階的主題,我們不會在這個練習中詳細討論。
如果你真的很好奇,可以在 Go 部落格:'append' 的機制 中讀到更多內容。
你附近的一所學校有個非常活躍的學生會。 學生會由會長管理,每 2 年會舉辦一次選舉,選出新任會長。
今年的選舉決定要採用一套新的數位計票系統。 學校需要你幫忙打造這套新系統。
新的投票系統首先需要的東西之一,就是計票器。 這個計數器可以用來追蹤某位候選人目前的票數。
建立一個函式NewVoteCounter,它接受某位候選人的初始票數,並回傳一個指向int的指標,以給定的初始票數初始化。
var initialVotes int
initialVotes = 2
var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true
現在你有辦法建立新的計數器了!但你發現新系統還需要一個方法,能從計數器取得票數。
建立一個函式VoteCount,它接受一個計數器(*int)作為引數,並回傳該計數器中的票數。
如果計數器是nil,就假設該計數器沒有任何票數:
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
VoteCount(voteCounter)
// => 3
var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0
終於要來計票了!現在你需要一個方法來增加計數器裡的票數。
建立一個函式IncrementVoteCount,它接受一個計數器(*int)和一個票數作為引數,並將計數器增加該票數。
你可以假設傳入的指標永遠不會是nil。
var votes int
votes = 3
var voteCounter *int
voteCounter = &votes
IncrementVoteCount(voteCounter, 2)
votes == 5 // true
*voteCounter == 5 // true
所有票都算完之後,該準備向全校公布結果了。 為此,你發現只有票數的計數器還不夠。 需要一個方法把票數和某位候選人關聯起來。
建立一個函式NewElectionResult,它接收候選人的名字和票數,並回傳一個新的選舉結果。
var result *ElectionResult
result = NewElectionResult("Peter", 3)
result.Name == "Peter" // true
result.Votes == 3 // true
選舉結果的結構體已經幫你建立好了,定義如下:
type ElectionResult struct {
// Name of the candidate
Name string
// Votes of votes the candidate had
Votes int
}
該向全校宣布新任會長了!
新任會長會透過學校裡的小型數位看板來公布。
訊息應該以下列格式顯示新任會長的名字和得票數:<candidate_name> (<votes>)。
這種訊息的範例如下:"Peter (51)"。
建立一個函式DisplayResult,它接收一個*ElectionResult作為引數,並回傳一個字串,內容是要顯示的訊息。
var result *ElectionResult
result = &ElectionResult{
Name: "John",
Votes: 32,
}
DisplayResult(result)
// => John (32)
為了確認最終結果準確無誤,票數重新計算了一次。 在重新計票時,發現某些候選人的票數差了一票。
建立一個函式DecrementVotesOfCandidate,它接收最終結果,以及一位要調降票數的候選人名字。
最終結果以map[string]int的形式提供,其中鍵是候選人的名字,值是該候選人的總票數。
var finalResults = map[string]int{
"Mary": 10,
"John": 51,
}
DecrementVotesOfCandidate(finalResults, "Mary")
finalResults["Mary"]
// => 9