Golang 程式設計教學:使用迴圈 (Loop) 或迭代控制結構 (Iteration Control Structure)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    在本文中,我們會使用迭代控制結構 (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 迴圈,以及三種改變迴圈行進流程的保留字,再附上控制系統資源的語法。透過這些特性,我們可以反覆執行重覆的任務,不需重寫相同的程式碼。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk