天天看點

《Go學習筆記 . 雨痕》流程控制(if、switch、for range、goto、continue、break)

Go 精簡(合并)了流控制語句,雖然某些時候不夠便捷,但夠用。

if...else...

條件表達式值必須是布爾類型,可省略括号,且左花括号不能另起一行。

func main()  {
	x := 3

	if x > 5 {
		println("a")
	} else if x < 5 && x > 0 {
		println("b")
	} else {
		println("z")
	}
}
      

比較特别的是對初始化語句的支援,可定義塊局部變量或執行初始化函數。

func main()  {
	x := 10

	if xinit(); x == 0 { // 優先執行 xinit 函數
		println("a")
	}

	if a, b := x + 1, x + 10; a < b { // 定義一個或多個局部變量(也可以是函數傳回值)
		println(a)
	} else {
		println(b)
	}
}      
局部變量的有效範圍包含整個 if/else 塊。

盡可能減少代碼塊嵌套,讓正常邏輯處于相同層次。

import (
	"errors"
	"log"
)

func check(x int) error {
	if x <= 0 {
		return errors.New("x <= 0")
	}

	return nil
}

func main()  {
	x := 10

	if err := check(x); err == nil {
		x++
		println(x)
	} else {
		log.Fatal(err)
	}
}      

該示例中,if 塊雖然承擔了 2種 邏輯:錯誤處理 和 後續正常操作。基于重構原則,我們應該保持代碼塊功能的單一性。

func check(x int) error {
	if x <= 0 {
		return errors.New("x <= 0")
	}

	return nil
}

func main() {
	x := 10

	if err := check(x); err != nil {
		log.Fatalln(err)
	}

	x++
	println(x)
}      

如此,if 塊僅完成條件檢查 和 錯誤處理,相關正常邏輯保持在同一層次。當有人視圖通過閱讀這段代碼來獲知邏輯流程時,完全可忽略 if 塊細節。同時,單一功能可提升代碼可維護性,更利于拆分重構。

當然,如須在多個條件塊中使用局部變量,那麼隻能保留層次,或直接使用外部變量。

func main() {
	s := "9"

	n, err := strconv.ParseInt(s, 10, 64) // 使用外部變量

	if err != nil {
		log.Fatalln(err)
	} else if n < 0 || n > 10 { // 也可以考慮拆分成另一個獨立 if 塊
		log.Fatalln("invalid number")
	}

	println(n) // 避免 if 局部變量将該邏輯放到 else 塊
}      

對應某些過于複雜的組合條件,建議将其重構為函數。

func main() {
	s := "9"

	if n, err := strconv.ParseInt(s, 10, 64); err != nil || n < 0 || n > 10 || n % 2 != 0 {
		log.Fatalln("invalid number")
	}

	println("ok")
}      

函數調用雖然有一些性能損失,可卻讓主流程式變得更加清爽。況且,條件語句獨立之後,更易于測試,同樣會改善代碼可維護性。

func check(s string) error {
	n, err := strconv.ParseInt(s, 10, 64)
	if err != nil || n < 0 || n > 10 || n%2 != 0 {
		return errors.New("invalid number")
	}

	return nil
}

func main() {
	s := "9"

	if err := check(s); err != nil {
		log.Fatalln(err)
	}

	println("ok")
}      

将 流程 和 布局 細節分離是很常見的做法,不同的變化因素被分隔在各自獨立單元(函數或子產品)内,可避免修改時造成關聯錯誤,減少患“肥胖症”的函數數量。當然,代碼單元測試也是主要原因之一。另一方面,該示例中的函數 check 僅被 if 塊調用,也可将其作為局部函數,以避免擴大作用域,隻是對測試的友好度會差一些。

目前編譯器隻能說夠用,須優化的地方太多,其中内聯處理做得也差強人意,是以代碼維護性 和 性能平衡需要投入更多心力。

語言方面,最遺憾的是沒有條件運算符 “a > b ? a : b”。有沒有 lambda 無所謂,但沒有這個卻少份優雅。加上一大堆 err != nil 判斷語句,對于有完美主義傾向的代碼潔癖患者來說是種折磨。

switch

與 if 類似,switch 語句也用于選擇執行,但具體使用場景會有所不同。

1、表達式 switch 語句

func main() {
	a, b, c, x := 1, 2, 3, 2

	switch x {				// 将 x 與 case 條件比對
	case a, b:				// 多個比對條件命中其一即可(OR),變量
		println("a | b")
	case c:					// 單個比對條件
		println("c")
	case 4:					// 常量
		println("d")
	default:
		println("z")
	}
}      

輸出:

a | b      
條件表達式支援非常量值,這要比 C 更加靈活。相比 if 表達式,switch 值清單要更加簡潔。編譯器對 if、switch 生成的機器指令可能完全相同,所謂誰性能更好須看具體情況,不能作為主觀判斷條件。

switch 同樣支援初始化語句,按從上到下、從左到右順序比對 case 執行。隻有全部比對失敗時,才會執行 default 塊。

func main() {
	switch x := 5; x {
	default:			// 編譯器確定不會先執行 default 塊
		x += 100
		println(x)
	case 5:
		x += 50
		println(x)
	}
}      
55      
考慮到 default 作用類似 else,建議将其放置在 switch 末尾。

相鄰的空 case 不構成多條件比對。

switch x {      // 單條件,内容為空。隐式 "case a: break;"
case a:
case b:
    println("b")
}      

不能出現重複的 case 常量值。

func main() {
	switch x := 5; x {
	case 5:
		println("a")
	case 6, 5:		// 錯誤:duplicate case 5 in switch
		println("b")
	}
}      

無須顯式執行 break 語句,case 執行完畢後自動中斷。如須貫通後續 case (源碼順序),須執行 fallthrough,但不再比對後續條件表達式。

func main() {
	switch x := 5; x {
	default:
		println(x)
	case 5:
		x += 10
		println(x)

		fallthrough		// 繼續執行下一個 case,但不再比對條件表達式
	case 6:
		x += 20
		println(x)

		//fallthrough	// 如果在此繼續 fallthrough,不會執行 default,完全按照源碼順序
						// 導緻 "cannot fallthrough final case in switch" 錯誤
	}
}      
15
35      
注意:fallthrough 必須放在 case 塊結尾,可使用 break 語句阻止。
func main() {
	switch x := 5; x {
	case 5:
		x += 10
		println(x)

		if x >= 15 {
			break		// 終止,不再執行後續語句
		}

		fallthrough		// 必須是 case 塊的最後一條語句
	case 6:
		x += 20
		println(x)
	}
}      
15      

某些時候,switch 還被用來替換 if 語句。被省略的 switch 條件表達式預設值為 true,繼而與 case 比較表達式結果比對。

func main() {
	switch x := 5; { // 相當于 "switch x :=5; true { ... }"
	case x > 5:
		println("a")
	case x > 0 && x <= 5: // 不能寫成 "case x > 0, x <= 5",因為多條件是 OR 關系
		println("b")
	default:
		println("c")
	}
}
      
b      

2、類型 switch 語句

類型 switch 語句 将對類型進行判定,而不是值。下面是一個簡單的例子:

var v interface{}
// 省略了部分代碼
// v = 8
// v = "wenjianbao"

switch v.(type) {
case string:
    fmt.Printf("The string is '%s'\n", v.(string))
case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
    fmt.Printf("The interger is %d\n", v)
default:
    fmt.Printf("Unsupporte value.(type=%T)\n", v)
}      

類型 switch 語句的 switch 表達式會包含一個特殊的類型斷言,例如 v.(type)。它雖然特殊,但是也要遵循類型斷言的規則。其次,每個 case 表達式中包含的都是 類型字面量,而不是表達式。最後,fallthrough 語句不允許出現在類型 switch 語句中。

類型 switch 語句的 switch 表達式還有一種變形寫法,如下:

var v interface{}
// 省略了部分代碼
// v = 8
// v = "wenjianbao"

switch i := v.(type) {
case string:
    fmt.Printf("The string is '%s'\n", i)
case int, uint, int8, uint8, int16, uint16, int32, uint32, int64, uint64:
    fmt.Printf("The interger is %d\n", i)
default:
    fmt.Printf("Unsupporte value.(type=%T)\n", i)
}      

這裡的 i := v.(type) 使經類型轉換後的值得以儲存。i 的類型一定會是 v 的值的實際類型。

for

僅有 for 一種循環語句,但常用方式都能支援。

for i := 0; i < 3; i++ { // 初始化表達式支援函數調用或定義局部變量
}      
for x < 10 { // 類似 "while x < 10 {}" 或 "for ; x < 10; {}"
    x++
}      
for {		// 類似 "while true {}" 或 "for true {}"
	break
}      

初始化語句僅被執行一次。條件表達式中如有函數調用,須确認是否會重複執行。可能會被編譯器優化掉,也可能是動态結果,須每次執行确認。

func count() int {
	print("count.")
	return 3
}

func main() {
	for i, c := 0, count(); i < c; i++ { // 初始化語句的 count() 函數僅執行一次
		println("a", i)
	}

	c := 0
	for c < count() { // 條件表達式中的 count 重複執行
		println("b", c)
		c++
	}
}
      
count.a 0
a 1
a 2
count.b 0
count.b 1
count.b 2
count.
Process finished with exit code 0
      
規避方式 就是在初始化表達式中定義局部變量儲存 count 結果。

可用 for ... range 完成資料疊代,支援 字元串、數組、數組指針、切片、字典、通道類型,傳回 索引、鍵值 資料。

data teyp 1st value 2nd value
string index s[index] unicode, rune
array/slice v[index]
map key value
channel element

# 疊代字元串:

func main() {
	str := "hello world"

	for index, ch := range str {
		fmt.Printf("%d -- %c\n", index, ch)
	}
}      
0 -- h
1 -- e
2 -- l
3 -- l
4 -- o
5 --  
6 -- w
7 -- o
8 -- r
9 -- l
10 -- d      

# 疊代數組

func main() {
	data := [3]string{"a", "b", "c"}

	for i, s := range data {
		println(i, s)
	}
}      
0 a
1 b
2 c      
沒有相關接口實作自定義類型疊代,除非基礎類型是上述類型之一。

允許傳回單值,或用 “_” 忽略。

func main() {
	data := [3]string{"a", "b", "c"}

	for i := range data { // 隻傳回 1st value
		println(i, data[i])
	}

	for _, s := range data { // 忽略 1st value
		println(s)
	}

	for range data { // 僅疊代,不傳回。可用來執行清空 channel 等操作
	}
}      

無論普通 for 循環,還是 range 疊代,其定義的局部變量都會 重複使用。

func main() {
	data := [3]string{"a", "b", "c"}

	for i, s := range data {
		println(&i, &s)
	}
}      
0xc42003bef0 0xc42003bf08
0xc42003bef0 0xc42003bf08
0xc42003bef0 0xc42003bf08      
這對 閉包 存在一些影響,相關詳情,請閱讀後續章節。

注意,range 會 複制 目标資料。受直接影響的是 數組,可改用 數組指針 或 切片 類型。

func main() {
	data := [3]int{10, 20, 30}

	for i, x := range data { // 從 data 複制品中取值
		if i == 0 {
			data[0] += 100
			data[1] += 200
			data[2] += 300
		}

		fmt.Printf("x: %d, data: %d\n", x, data[i])
	}

	for i, x := range data[:] { // 僅複制 slice,不包括 底層 array
		if i == 0 {
			data[0] += 100
			data[1] += 200
			data[2] += 300
		}

		fmt.Printf("x: %d, data: %d\n", x , data[i])
	}
}      
x: 10, data: 110
x: 20, data: 220    // range 傳回的依舊是複制值
x: 30, data: 330

x: 110, data: 210   // 當 i == 0 修改 data 時,x 已經取值,是以是 110
x: 420, data: 420   // 複制的僅是 slice 自身,底層 array 依舊是原對象
x: 630, data: 630      
相關資料類型中,字元串、切片基本結構是個很小的結構體,而 字典、通道 本身是指針封裝,複制成本都很小,無須專門優化。

如果 range 目标表達式是函數調用,也僅被執行一次。

func data() []int {
	println("origin data.")
	return []int{10, 20, 30}
}

func main() {
	for i, x := range data() {
		println(i, x)
	}
}      
origin data.
0 10
1 20
2 30      

建議嵌套循環不要超過 2 層,否則會難以維護。必要時可剝離,重構為函數。

使用 range 子句,有 3 點需要注意,如下:

  • 若對數組、切片 或 字元串值進行疊代,且 := 左邊隻有一個疊代變量時,一定要小心。這時隻會得到其中元素的索引,而不是元素本身;這很可能并不是你想要的。
  • 疊代沒有任何元素的數組值、為 nil 的切片值、為 nil 的字典值 或 為 "" 的字元串值,并不會執行 for 語句中的代碼。for 語句在一開始就會直接結束執行。因為這些值的長度都為 0。
  • 疊代為 nil 的通道值 會讓目前流程永遠阻塞在 for 語句上!

goto,continue,break

對于 goto 的讨伐由來已久,仿佛它是“笨蛋”标簽一般。可事實上,能在很多場合見到

它的身影,就連 Go 源碼裡都有很多。

$ cd go/src
$ grep -r -n "goto"  *      
單就 Go 1.6 的源碼統計結果,goto 語句就超過 1000 條有餘。很驚訝,不是嗎?雖然某些設計模式可用來消除 goto 語句,但在性能優先的場合,它能發揮積極作用。

使用 goto 前,須先定義标簽。标簽區分大小寫,且未使用的标簽會引發編譯錯誤。

func main() {
start:							// 錯誤:label start defined and note used
	for i := 0; i < 3; i++ {
		println(i)

		if i > 1 {
			goto exit
		}
	}
exit:
	println("exit.")
}      

不能跳轉到其他函數,或内層代碼塊内。

func test() {
test:
	println("test")
	println("test exit.")
}

func main() {
	for i := 0; i < 3; i++ {
	loop:
		println(i)
	}

	goto test		// 錯誤:label test not defined
	goto loop		// 錯誤:goto loop jumps into block
}      

和 goto 定義跳轉不同,break、continue 用于中斷代碼執行。

  • break:用于 switch、for、select 語句,終止整個語句塊執行。
  • continue:僅用于 for 循環,終止後續邏輯,立即進入下一輪循環。
func main() {
	for i := 0; i < 10; i++ {
		if i%2 == 0 {
			continue		// 立即進入下一輪循環
		}

		if i > 5 {
			break			// 立即終止整個 for 循環
		}

		println(i)
	}
}      
1
3
5      
func main() {
outer:
	for x := 0; x < 5; x++ {
		for y := 0; y < 10; y++ {
			if y > 2 {
				println()
				continue outer
			}

			if x > 2 {
				break outer
			}

			print(x, ":", y, " ")
		}
	}
}
      
0:0 0:1 0:2 
1:0 1:1 1:2 
2:0 2:1 2:2