前言
在本文中,我們會使用迭代控制結構 (iteration control structure) 來達成反覆 (repeating) 或循環 (looping) 的行為,藉以省下重覆的程式碼。
主流的程式語言會用兩至三個保留字來表達不同情境的迭代,但 Go 語言卻使用同一個保留字 for
來表達三種不同的迴圈。有些程式人覺得 Go 語言在這裡精簡過頭,但 Go 語言為了向後相容性,短時間內不會改掉這項特性。閱讀 Go 程式碼時要辨識當下的 for
是使用何種情境。
使用條件句的 for
第一種 for
迴圈使用條件句 (conditional) 做為迴圈終止條件。參考以下 Go 虛擬碼:
for cond {
// Run code here repeatedly.
}
在這個 for
迴圈中,只要 cond
為真,for
區塊內的程式碼就會不間斷地反覆執行。反之,當 cond
不為真時,則 for
區塊會終止。我們會透過改變程式的狀態,讓 for
迴圈執行一定次數後停止。
以下是一個特例:
for {
// Run code here infinitely.
}
在這個例子中,for
迴圈會無限次地執行。這樣的迴圈稱為無限迴圈 (infinite loop)。無限迴圈可能是初心者寫錯迴圈所造成的,也可能視程式的需求刻意使用無限迴圈。像是電腦遊戲會用遊戲迴圈 (game loop) 持續地執行遊戲,直到遊戲結束 (game over);遊戲迴圈本質上是一個大型的無限迴圈。
在 Go 語言使用無限迴圈時,會搭配 break
來終止迴圈。後文會介紹 break
。
以下是一個簡短的實例:
package main
import "fmt"
func main() {
i := 1
for i <= 10 {
fmt.Println(i)
i++
}
}
由於本範例使用條件句做為迴圈終止條件,i
的狀態要自行用程式更新。故我們用 i++
每次迭代時將 i
遞增 1
。
這個例字實際上印出的內容如下:
1
2
3
4
5
6
7
8
9
10
使用計數器的 for
第二種 for
迴圈使用計數器 (counter) 來做為迴圈的中止條件。我們看一下以下的虛擬碼:
for init; end; next {
// Run code here repeatedly.
}
for
迴圈實際上有四個區塊。除了實際執行的程式碼區塊外,for
迴圈的條件句由三個小區塊組成。在 init
區塊內將計數器初始化;當計數器仍滿足 end
區塊內的條件,迴圈就會繼續執行;在每次迭代中,在 next
區塊內改變計數器的狀態。
延續上述虛擬碼,我們來看計數器的部分如何寫:
for counter := 0; counter < end; counter++ {
// Run code here repeatedly.
}
在這段 Go 虛擬碼範例中,一開始先將 counter
初始化為 0
,在每次的迭代中將 counter
遞增 1
,當 counter
大於等於 end
時終止迴圈。
以下是一個簡短的實例:
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
fmt.Println(i)
}
}
在實務上,我們會在迴圈中用 i
這種簡短的識別字。因為計數器只是一個暫時的變數,沒有實質的意義,使用簡短的識別字會讓程式碼看起來更乾淨。
這個範例同樣會從 1
印到 10
:
1
2
3
4
5
6
7
8
9
10
在撰寫迴圈時,什麼時候使用條件句而什麼時候使用計數器呢?當迴圈有明確的執行次數時,使用計數器較佳;反之,則使用條件句。
使用迭代器的 for
藉由迭代器 (iterator),電腦程式可在不處理資料結構 (或容器) 內部的細節就可以走訪該資料結構的元素。Go 語言僅對內建的資料結構,如陣列、切片、映射等,支援隱性的迭代器。Go 語言的迭代器會搭配 for
迴圈來使用。我們會在後續介紹到各個資料結構時介紹如何搭配 for
迴圈。
用 break
提早離開迴圈
break
用於提早結束迴圈。我們看一下以下的實例:
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
if 5 < i {
break
}
fmt.Println(i)
}
}
這個範例會由 1
印到 5
:
1
2
3
4
5
因為 i
大於 5
時,會觸發 break
敘述,終止 for
迴圈。
用 continue
跳過單次迭代
continue
用來跳過單次的迭代。我們來看以下的實例:
package main
import "fmt"
func main() {
for i := 1; i <= 10; i++ {
if i%2 == 0 {
continue
}
fmt.Println(i)
}
}
這個範例只會印出奇數:
1
3
5
7
9
當 i
是偶數時,會觸發 continue
敘述,把該次迭代跳過,故不會印出數字。
goto
是必然之惡
goto
是終極的控制結構,因為可以使用 goto
任意移動到同函式中其他位置。像是以下範例用 goto
模擬 break
:
package main
import "fmt"
func main() {
i := 1
for i <= 10 {
if i > 5 {
goto END
}
fmt.Println(i)
i++
}
END:
}
在這個例子中,當 i
大於 5
時,觸發 goto
敘述,跳到 END
標籤所在的位置。整體的效果如同 break
。
以下例子則用 goto
模擬 continue
:
package main
import "fmt"
func main() {
i := 1
LOOP:
for i <= 10 {
if i%2 == 0 {
i++
goto LOOP
}
fmt.Println(i)
i++
}
}
在這個例子中,當 i
為偶數時,觸發 goto
敘述,跳到 LOOP
標籤所在的位置。整體的效果等同於 continue
。
雖然有些程式人視 goto
為邪惡的語法特性,甚至有些程式語言直接封印 goto
。但適當地使用 goto
,會讓程式碼更簡潔。Go 原始碼中也有用到 goto
,像是 gamma.go 中的片段:
for x < 0 {
if x > -1e-09 {
goto small
}
z = z / x
x = x + 1
}
for x < 2 {
if x < 1e-09 {
goto small
}
z = z / x
x = x + 1
}
if x == 2 {
return z
}
x = x - 2
p = (((((x*_gamP[0]+_gamP[1])*x+_gamP[2])*x+_gamP[3])*x+_gamP[4])*x+_gamP[5])*x + _gamP[6]
q = ((((((x*_gamQ[0]+_gamQ[1])*x+_gamQ[2])*x+_gamQ[3])*x+_gamQ[4])*x+_gamQ[5])*x+_gamQ[6])*x + _gamQ[7]
return z * p / q
small:
if x == 0 {
return Inf(1)
}
return z / ((1 + Euler*x) * x)
在這個片段中,有兩個 for
迴圈共用同一個 small
片段的程式碼。若沒有 goto
這項特性,反而程式碼會變得更複雜。
用 defer
優雅地處理系統資源
C 程式可用 goto
來處理系統資源的釋放。但在 Go 語言中,可用 defer
取代 goto
來處理系統資源的釋放。參考以下 Go 程式片段:
f, err := os.Create("file.txt")
if err != nil {
log.Fatal(err)
}
defer f.Close()
在本片段中,我們建立一個檔案物件 f
,之後用 defer
來觸發關閉 f
物件的指令。defer
敘述會自動延遲到 defer
所在的函式結束時才觸發指令,不需要手動控制程式流程,所以會比直接用 goto
敘述簡單得多。
結語
在本文中,我們介紹了三種不同終止條件的 for
迴圈,以及三種改變迴圈行進流程的保留字,再附上控制系統資源的語法。透過這些特性,我們可以反覆執行重覆的任務,不需重寫相同的程式碼。