?年學 go 2:控制流

goodspeed發表於2018-04-17

上一篇我們瞭解了golang 的變數、函式和基本型別,這一篇將介紹一下控制流

現在我們看一個複雜點的例子:

fibonacci(遞迴版)

package main
import "fmt"

func main() {
    result := 0
    for i := 0; i <= 10; i++ {
	     result = fibonacci(i)
	     fmt.Printf("fibonacci(%d) is: %d\n", i, result)
	  }
}

func fibonacci(n int) (res int) {
    if n <= 1 {
        res = 1
	   } else {
	       res = fibonacci(n-1) + fibonacci(n-2)
	   }
	return
}

    // outputs

    fibonacci(0) is: 1
    fibonacci(1) is: 1
    fibonacci(2) is: 2
    fibonacci(3) is: 3
    fibonacci(4) is: 5
    fibonacci(5) is: 8
    fibonacci(6) is: 13
    fibonacci(7) is: 21
    fibonacci(8) is: 34
    fibonacci(9) is: 55
    fibonacci(10) is: 89
複製程式碼
  • for i := 0; i <= 10; i++ {} 第7行是一個迴圈結構 這裡for 迴圈是一個控制流

控制流

For

Go 只有一種迴圈介面-- for 迴圈

For 支援三種迴圈方式,包括類 while 語法

1 基本for迴圈 支援初始化語句

s := "abc"
for i, n := 0, len(s); i < n; i++ {
	// i, n 為定義的變數 只在for 迴圈內作用
    println(s[i])
}
複製程式碼

基本的 for 迴圈包含三個由分號分開的組成部分:

  • 初始化語句:在第一次迴圈執行前被執行
  • 迴圈條件表示式:每輪迭代開始前被求值
  • 後置語句:每輪迭代後被執行

2 替代 while (n > 0) {}

C 的 while 在 Go 中叫做 for

n := len(s)
// 迴圈初始化語句和後置語句都是可選的。
for n > 0 { // 等同於 for (; n > 0;) {}
    println(s[n])
    n--
}
複製程式碼

3 死迴圈

    for { // while true
        println(s)
    }
複製程式碼

IF…ELSE

就像 for 迴圈一樣,Go 的 if 語句也不要求用 ( ) 將條件括起來,同時, { } 還是必須有的

  • 條件表示式必須是布林型別,可省略條件表示式括號
  • 支援初始化語句,可定義程式碼塊區域性變數
  • 程式碼塊左大括號必須在條件表示式尾部
x := 0

// if x > 10  // Error: missing condition in if statement(左大括號必須在條件表示式尾部)
// {
// }

if x > 10{
    ...
}else{
    ...
}

if n := "abc"; x > 0 {  // 初始化語句(在這裡是定義變數)
	println(n[2])
} else if x < 0 {
	println(n[1])
} else {
    println(n[0])     // 區域性變數 n 有效範圍是 整個 if/else 塊
}
複製程式碼

if 語句定義的變數作用域僅在if範圍之內(包含else語句) 不支援三元操作符 "a > b ? a : b"

以上是上段程式碼出現的兩個控制流,剩下的控制流還有

  • Switch
  • Range
  • Goto, Break, Continue, defer

Switch

switch 語句用於選擇執行,語法如下:

    switch optionalStatement; optionalExpression{
		case expressionList1: block1
		...
		case expressionListN: blockN
		default: blockD
	}
複製程式碼

先看一個例子:

package main

import (
	"fmt"
	"runtime"
)

func main() {
	fmt.Print("Go runs on ")
	switch os := runtime.GOOS; os { // 將 os 與 case 條件匹配
	case "darwin":
	    fmt.Println("OS X.")
	case "linux":
	    fmt.Println("Linux.")
	case "plan9", "openbsd": // 多個條件命中其一即可(OR)
	    fmt.Println("plan9 | openbsd")
	default:
		// freebsd, openbsd,
		// plan9, windows...
		fmt.Printf("%s.", os)
	}
}

複製程式碼
  • 如果有可選語句宣告, 分號是必要的, 無論後邊的可選表示式語句是否出現(如果可選語句沒有出現預設為true)
  • 每一個case 語句必須要有一個表示式列表,多個用分號隔開
  • switch 語句自上而下執行,當匹配成功後執行case分支的程式碼塊,執行結束後退出switch
switch i {
case 0: // 空分支,只有當 i == 0 時才會進入分支 相當於 "case 0: break;"
case 1:
    f() // 當 i == 0 時函式不會被呼叫
}
複製程式碼
  • 如果想要在執行完每個分支的程式碼後還繼續執行後續的分支程式碼,可以使用fallthrough 關鍵字達到目的
package main

import "fmt"

func switch1(n int) {
	switch { // 這裡用的是沒有條件的switch 語句會直接執行
	case n == 0:
	    fmt.Println(0)
	    fallthrough  // fallthrough 需放在 case 塊結尾,可用 break 阻止
	case n == 1: // 如果匹配到0 這裡會繼續執行
	    fmt.Println(1)
	case n == 2: // fallthrough 不會對這裡有作用
	    fmt.Println(2)
	default:
	    fmt.Println("default")
	}
}

func main() {
	switch1(0)
}
# output
0
1
複製程式碼
  • 用 default 可以指定當其他所有分支都不匹配的時候的行為
    switch i {
    	case 0:
    	case 1:
    		f()
    	default:
    		g()  // 當i不等於0 或 1 時呼叫
    }
複製程式碼

Range

Range 類似迭代器的操作,返回(索引,值)或(健,值)

它可以迭代任何一個集合(包括字串、陣列、陣列指標、切片、字典、通道)

基本語法如下:

coll := 3string["a", "b", "c"]
for ix, val := range coll {
   ...
}

// 允許返回單值

for ix := range coll {
    println(ix, coll[ix])
}

// 也可以使用 _ 忽略

for _, val := range coll {
    println(val)
}

// 也可以只迭代,不返回。可用來執行清空 channel 等操作

for range coll {
    ...
}
複製程式碼

val 始終為集合中對應索引的值拷貝,因此它一般只具有隻讀性質,對它所做的任何修改都不會影響到集合中原有的值(譯者注:如果 val 為指標,則會產生指標的拷貝,依舊可以修改集合中的原值 一個字串是 Unicode 編碼的字元(或稱之為 rune)集合,因此您也可以用它迭代字串

下面是每種資料型別使用range時 ix和val 的值

date type ix value 值型別
string index s[index] unicode, rune
array/slice index s[index]
map key m[index]
channel element

range 會複製目標資料。字串、切片基本結構是個很小的結構體,而字典、通道本身是指標封裝,複製成本很小,無需專門優化。

如果是陣列,可改成陣列指標或者切片型別。

Break continue

break 和 continue 都可在多級巢狀迴圈中跳出

break 可用於 for、switch、select語句,終止整個語句塊執行

continue 僅能 於 for 迴圈,終止後續操作,立即進入下一輪迴圈。

goto

goto 語句可以配合標籤(label)形式的識別符號使用,即某一行第一個以冒號:結尾的單詞,標籤區分大小寫。

package main

func main() {
    i:=0
    HERE:
	    print(i)
		i++
        if i==5 {
	        return
	    }
        goto HERE
}
# output 01234
複製程式碼

使用標籤和 goto 語句是不被鼓勵的:它們會很快導致非常糟糕的程式設計,而且總有更加可讀的替代方案來實現相同的需求。

for、switch 或 select 語句都可以配合標籤(label)形式的識別符號使用

package main

import "fmt"

func main() {

LABEL1:
    for i := 0; i <= 5; i++ {
		for j := 0; j <= 5; j++ {
			if j == 4 {
				continue LABEL1
			}
			fmt.Printf("i is: %d, and j is: %d\n", i, j)
		}
	}
}

複製程式碼

continue 語句指向 LABEL1,當執行到該語句的時候,就會跳轉到 LABEL1 標籤的位置

defer

defer 語句會延遲函式的執行直到上層函式返回

延遲呼叫的引數會立刻生成,但是在上層函式返回前函式都不會被呼叫

package main

import "fmt"

func main() {
	defer fmt.Println("world")

	fmt.Println("hello")
}

// output
hello
world
複製程式碼

defer 棧

延遲的函式呼叫被壓入一個棧中。當函式返回時, 會按照後進先出的順序呼叫被延遲的函式呼叫。 defer 常用來定義簡單的方法

package main

import "fmt"

func main() {
	fmt.Println("counting")

	for i := 0; i < 10; i++ {
			defer fmt.Println(i)
		}

		fmt.Println("done")
}
// 可以想一下會輸出什麼
// 程式碼執行 https://tour.go-zh.org/flowcontrol/13
複製程式碼

關鍵字 defer 允許我們進行一些函式執行完成後的收尾工作,例如:

  • 關閉檔案流:

    // open a file defer file.Close()

  • 解鎖一個加鎖的資源

    mu.Lock() defer mu.Unlock()

  • 列印最終報告

    printHeader() defer printFooter()

  • 關閉資料庫連結

    // open a database connection defer disconnectFromDB()

合理使用 defer 語句能夠使得程式碼更加簡潔。

下面的程式碼展示了在除錯時使用 defer 語句的手法

package main

import (
	"io"
    "log"
)

func func1(s string) (n int, err error) {
    defer func() {
	        log.Printf("func1(%q) = %d, %v", s, n, err)
	    }()
    return 7, io.EOF
}

func main() {
    func1("Go")
}

// 輸出
Output: 2016/04/25 10:46:11 func1("Go") = 7, EOF
複製程式碼

更多defer 的用法(blog.go-zh.org/defer-panic…)

參考連結

Go 指南 The way to go -- 控制結構 Effective Go

到這裡簡單的控制流用法講解就結束了

下節將會是golang 資料結構部分, 會用到的程式碼為

fibonacci(記憶體版)

 package main

 import (
     "fmt"
     "time"
 )

 const LIM = 41

 var fibs [LIM]uint64

 func main() {
     var result uint64 = 0
     start := time.Now()
     for i := 0; i < LIM; i++ {
         result = fibonacci(i)
         fmt.Printf("fibonacci(%d) is: %d\n", i, result)
     }
     end := time.Now()
     delta := end.Sub(start)
     fmt.Printf("longCalculation took this amount of time: %s\n", delta)
 }
 func fibonacci(n int) (res uint64) {
     // memoization: check if fibonacci(n) is already known in array:
     if fibs[n] != 0 {
         res = fibs[n]
         return
     }
     if n <= 1 {
         res = 1
     } else {
         res = fibonacci(n-1) + fibonacci(n-2)
     }
     fibs[n] = res
     return
 }
複製程式碼

最後,感謝女朋友支援和包容,比❤️

想了解以下內容可以在公號輸入相應關鍵字獲取歷史文章: 公號&小程式 | 設計模式 | 併發&協程

?年學 go 2:控制流

相關文章