學習軌道
/
Go
Go
/
練習
/
選舉日
選舉日

選舉日

學習練習

簡介

和許多其他語言一樣,Go 也有指標。 如果你剛開始接觸指標,可能會覺得它們有點神祕,但一旦習慣之後,就會發現它們其實相當直觀。 指標是 Go 很關鍵的一部分,所以請花點時間好好理解它們吧。

在深入細節之前,先了解指標的用途很值得。 指標是一種和程式其他部分共享記憶體的方式,它在兩個主要情況下很有用:

  1. 當我們有大量資料時,在函式之間傳遞時製作副本非常沒有效率。 改成傳遞資料儲存位置的記憶體位置,就能大幅減少程式的資源占用。
  2. 在函式之間傳遞指標,我們就能直接存取並修改那份唯一的資料副本。也就是說,某個函式所做的任何變更,在函式結束時會立刻被程式的其他部分看見。

變數與記憶體

假設我們有一個普通的整數變數 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 年會舉辦一次選舉,選出新任會長。

今年的選舉決定要採用一套新的數位計票系統。 學校需要你幫忙打造這套新系統。

1. 建立計票器

新的投票系統首先需要的東西之一,就是計票器。 這個計數器可以用來追蹤某位候選人目前的票數。

建立一個函式NewVoteCounter,它接受某位候選人的初始票數,並回傳一個指向int的指標,以給定的初始票數初始化。

var initialVotes int
initialVotes = 2

var counter *int
counter = NewVoteCounter(initialVotes)
*counter == initialVotes // true

2. 從計數器取得票數

現在你有辦法建立新的計數器了!但你發現新系統還需要一個方法,能從計數器取得票數。

建立一個函式VoteCount,它接受一個計數器(*int)作為引數,並回傳該計數器中的票數。 如果計數器是nil,就假設該計數器沒有任何票數:

var votes int
votes = 3

var voteCounter *int
voteCounter = &votes

VoteCount(voteCounter)
// => 3

var nilVoteCounter *int
VoteCount(nilVoteCounter)
// => 0

3. 增加計數器的票數

終於要來計票了!現在你需要一個方法來增加計數器裡的票數。

建立一個函式IncrementVoteCount,它接受一個計數器(*int)和一個票數作為引數,並將計數器增加該票數。 你可以假設傳入的指標永遠不會是nil。

var votes int
votes = 3

var voteCounter *int
voteCounter = &votes

IncrementVoteCount(voteCounter, 2)

votes == 5          // true
*voteCounter == 5   // true

4. 建立選舉結果

所有票都算完之後,該準備向全校公布結果了。 為此,你發現只有票數的計數器還不夠。 需要一個方法把票數和某位候選人關聯起來。

建立一個函式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
}

5. 宣布結果

該向全校宣布新任會長了! 新任會長會透過學校裡的小型數位看板來公布。 訊息應該以下列格式顯示新任會長的名字和得票數:<candidate_name> (<votes>)。 這種訊息的範例如下:"Peter (51)"。

建立一個函式DisplayResult,它接收一個*ElectionResult作為引數,並回傳一個字串,內容是要顯示的訊息。

var result *ElectionResult
result = &ElectionResult{
    Name: "John",
    Votes: 32,
}

DisplayResult(result)
// => John (32)

6. 重新計票

為了確認最終結果準確無誤,票數重新計算了一次。 在重新計票時,發現某些候選人的票數差了一票。

建立一個函式DecrementVotesOfCandidate,它接收最終結果,以及一位要調降票數的候選人名字。 最終結果以map[string]int的形式提供,其中鍵是候選人的名字,值是該候選人的總票數。

var finalResults = map[string]int{
    "Mary":  10,
    "John":  51,
}

DecrementVotesOfCandidate(finalResults, "Mary")

finalResults["Mary"]
// => 9
透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Go Exercism

準備好開始 選舉日 了嗎?

註冊 Exercism,透過 34 個概念165 個練習 和真人引導來學習並精通 Go,全部免費。