Go語言流程控制結構和函式解析

codeceo發表於2015-03-05

和其他程式語言類似,Go語言也有自己的流程控制結構,比如條件分支、迴圈語句等。另外,Go語言中的函式也需要一些關鍵字修飾,從而實現不同的函式功能。本文就一起來探討一下Go語言的流程控制結構和函式。

流程控制

流程控制在程式語言中是最偉大的發明了,因為有了它,你可以通過很簡單的流程描述來表達很複雜的邏輯。Go中流程控制分三大類:條件判斷,迴圈控制和無條件跳轉。

if

if也許是各種程式語言中最常見的了,它的語法概括起來就是:如果滿足條件就做某事,否則做另一件事。

Go裡面if條件判斷語句中不需要括號,如下程式碼所示:

if x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}

Go的if還有一個強大的地方就是條件判斷語句裡面允許宣告一個變數,這個變數的作用域只能在該條件邏輯塊內,其他地方就不起作用了,如下所示:

// 計算獲取值x,然後根據x返回的大小,判斷是否大於10。
if x := computedValue(); x > 10 {
    fmt.Println("x is greater than 10")
} else {
    fmt.Println("x is less than 10")
}
//這個地方如果這樣呼叫就編譯出錯了,因為x是條件裡面的變數
fmt.Println(x)

多個條件的時候如下所示:

if integer == 3 {
    fmt.Println("The integer is equal to 3")
} else if integer < 3 {
    fmt.Println("The integer is less than 3")
} else {
    fmt.Println("The integer is greater than 3")
}

goto

Go有goto語句——請明智地使用它。用goto跳轉到必須在當前函式內定義的標籤。例如假設這樣一個迴圈:

func myFunc() {
    i := 0
Here:   //這行的第一個詞,以冒號結束作為標籤
    println(i)
    i++
    goto Here   //跳轉到Here去
}

標籤名是大小寫敏感的。

for

Go裡面最強大的一個控制邏輯就是for,它即可以用來迴圈讀取資料,又可以當作while來控制邏輯,還能迭代操作。它的語法如下:

for expression1; expression2; expression3 {
    //...
}

expression1、expression2和expression3都是表示式,其中expression1和expression3是變數宣告或者函式呼叫返回值之類的,expression2是用來條件判斷,expression1在迴圈開始之前呼叫,expression3在每輪迴圈結束之時呼叫。

一個例子比上面講那麼多更有用,那麼我們看看下面的例子吧:

package main
import "fmt"
func main(){
    sum := 0;
    for index:=0; index < 10 ; index++ {
        sum += index
    }
    fmt.Println("sum is equal to ", sum)
}
// 輸出:sum is equal to 45

有些時候需要進行多個賦值操作,由於Go裡面沒有,操作符,那麼可以使用平行賦值i, j = i+1, j-1

有些時候如果我們忽略expression1和expression3:

sum := 1
for ; sum < 1000;  {
    sum += sum
}

其中;也可以省略,那麼就變成如下的程式碼了,是不是似曾相識?對,這就是while的功能。

sum := 1
for sum < 1000 {
    sum += sum
}

在迴圈裡面有兩個關鍵操作break和continue ,break操作是跳出當前迴圈,continue是跳過本次迴圈。當巢狀過深的時候,break可以配合標籤使用,即跳轉至標籤所指定的位置,詳細參考如下例子:

for index := 10; index>0; index-- {
    if index == 5{
        break // 或者continue
    }
    fmt.Println(index)
}
// break列印出來10、9、8、7、6
// continue列印出來10、9、8、7、6、4、3、2、1

break和continue還可以跟著標號,用來跳到多重迴圈中的外層迴圈

for配合range可以用於讀取slice和map的資料:

for k,v:=range map {
    fmt.Println("map's key:",k)
    fmt.Println("map's val:",v)
}

switch

有些時候你需要寫很多的if-else來實現一些邏輯處理,這個時候程式碼看上去就很醜很冗長,而且也不易於以後的維護,這個時候switch就能很好的解決這個問題。它的語法如下

switch sExpr {
case expr1:
    some instructions
case expr2:
    some other instructions
case expr3:
    some other instructions
default:
    other code
}

sExpr和expr1、expr2、expr3的型別必須一致。Go的switch非常靈活,表示式不必是常量或整數,執行的過程從上至下,直到找到匹配項;而如果switch沒有表示式,它會匹配true。

i := 10
switch i {
case 1:
    fmt.Println("i is equal to 1")
case 2, 3, 4:
    fmt.Println("i is equal to 2, 3 or 4")
case 10:
    fmt.Println("i is equal to 10")
default:
    fmt.Println("All I know is that i is an integer")
}

在第5行中,我們把很多值聚合在了一個case裡面,同時,Go裡面switch預設相當於每個case最後帶有break,匹配成功後不會自動向下執行其他case,而是跳出整個switch, 但是可以使用fallthrough強制執行後面的case程式碼。

integer := 6
switch integer {
    case 4:
    fmt.Println("The integer was <= 4")
    fallthrough
    case 5:
    fmt.Println("The integer was <= 5")
    fallthrough
    case 6:
    fmt.Println("The integer was <= 6")
    fallthrough
    case 7:
    fmt.Println("The integer was <= 7")
    fallthrough
    case 8:
    fmt.Println("The integer was <= 8")
    fallthrough
    default:
    fmt.Println("default case")
}

上面的程式將輸出

The integer was <= 6
The integer was <= 7
The integer was <= 8
default case

函式

函式是Go裡面的核心設計,它通過關鍵字func來宣告,它的格式如下:

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
    //這裡是處理邏輯程式碼
    //返回多個值
    return value1, value2
}

上面的程式碼我們看出

1.關鍵字func用來宣告一個函式funcName
2.函式可以有一個或者多個引數,每個引數後面帶有型別,通過,分隔
3.函式可以返回多個值
4.上面返回值宣告瞭兩個變數output1和output2,如果你不想宣告也可以,直接就兩個型別
5.如果只有一個返回值且不宣告返回值變數,那麼你可以省略 包括返回值 的括號
6.如果沒有返回值,那麼就直接省略最後的返回資訊
7.如果有返回值, 那麼必須在函式的外層新增return語句

下面我們來看一個實際應用函式的例子(用來計算Max值)

package main
import "fmt"
// 返回a、b中最大值.
func max(a, b int) int {
    if a > b {
        return a
    }
    return b
}
func main() {
    x := 3
    y := 4
    z := 5
    max_xy := max(x, y) //呼叫函式max(x, y)
    max_xz := max(x, z) //呼叫函式max(x, z)
    fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
    fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
    fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // 也可在這直接呼叫它
}

上面這個裡面我們可以看到max函式有兩個引數,它們的型別都是int,那麼第一個變數的型別可以省略(即 a,b int,而非 a int, b int),預設為離它最近的型別,同理多於2個同型別的變數或者返回值。同時我們注意到它的返回值就是一個型別,這個就是省略寫法。

多個返回值

Go語言比C更先進的特性,其中一點就是函式能夠返回多個值。

我們直接上程式碼看例子:

package main
import "fmt"
//返回 A+B 和 A*B
func SumAndProduct(A, B int) (int, int) {
    return A+B, A*B
}
func main() {
    x := 3
    y := 4
    xPLUSy, xTIMESy := SumAndProduct(x, y)
    fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
    fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

上面的例子我們可以看到直接返回了兩個引數,當然我們也可以命名返回引數的變數,這個例子裡面只是用了兩個型別,我們也可以改成如下這樣的定義,然後返回的時候不用帶上變數名,因為直接在函式裡面初始化了。但如果你的函式是匯出的(首字母大寫),官方建議:最好命名返回值,因為不命名返回值,雖然使得程式碼更加簡潔了,但是會造成生成的文件可讀性差。

func SumAndProduct(A, B int) (add int, Multiplied int) {
    add = A+B
    Multiplied = A*B
    return
}

變參

Go函式支援變參。接受變參的函式是有著不定數量的引數的。為了做到這點,首先需要定義函式使其接受變參:

func myfunc(arg ...int) {}

arg …int告訴Go這個函式接受不定數量的引數。注意,這些引數的型別全部是int。在函式體中,變數arg是一個int的slice:

for _, n := range arg {
    fmt.Printf("And the number is: %d\n", n)
}

傳值與傳指標

當我們傳一個引數值到被呼叫函式裡面時,實際上是傳了這個值的一份copy,當在被呼叫函式中修改引數值的時候,呼叫函式中相應實參不會發生任何變化,因為數值變化只作用在copy上。

為了驗證我們上面的說法,我們來看一個例子

package main
import "fmt"
//簡單的一個函式,實現了引數+1的操作
func add1(a int) int {
    a = a+1 // 我們改變了a的值
    return a //返回一個新值
}
func main() {
    x := 3
    fmt.Println("x = ", x)  // 應該輸出 "x = 3"
    x1 := add1(x)  //呼叫add1(x)
    fmt.Println("x+1 = ", x1) // 應該輸出"x+1 = 4"
    fmt.Println("x = ", x)    // 應該輸出"x = 3"
}

看到了嗎?雖然我們呼叫了add1函式,並且在add1中執行a = a+1操作,但是上面例子中x變數的值沒有發生變化

理由很簡單:因為當我們呼叫add1的時候,add1接收的引數其實是x的copy,而不是x本身。

那你也許會問了,如果真的需要傳這個x本身,該怎麼辦呢?

這就牽扯到了所謂的指標。我們知道,變數在記憶體中是存放於一定地址上的,修改變數實際是修改變數地址處的記憶體。只有add1函式知道x變數所在的地址,才能修改x變數的值。所以我們需要將x所在地址&x傳入函式,並將函式的引數的型別由int改為*int,即改為指標型別,才能在函式中修改x變數的值。此時引數仍然是按copy傳遞的,只是copy的是一個指標。請看下面的例子:

package main
import "fmt"
//簡單的一個函式,實現了引數+1的操作
func add1(a *int) int { // 請注意,
    *a = *a+1 // 修改了a的值
    return *a // 返回新值
}
func main() {
    x := 3
    fmt.Println("x = ", x)  // 應該輸出 "x = 3"
    x1 := add1(&x)  // 呼叫 add1(&x) 傳x的地址
    fmt.Println("x+1 = ", x1) // 應該輸出 "x+1 = 4"
    fmt.Println("x = ", x)    // 應該輸出 "x = 4"
}

這樣,我們就達到了修改x的目的。那麼到底傳指標有什麼好處呢?

1.傳指標使得多個函式能操作同一個物件。
2.傳指標比較輕量級 (8bytes),只是傳記憶體地址,我們可以用指標傳遞體積大的結構體。如果用引數值傳遞的話, 在每次copy上面就會花費相對較多的系統開銷(記憶體和時間)。所以當你要傳遞大的結構體的時候,用指標是一個明智的選擇。
3.Go語言中string,slice,map這三種型別的實現機制類似指標,所以可以直接傳遞,而不用取地址後傳遞指標。(注:若函式需改變slice的長度,則仍需要取地址傳遞指標)

defer

Go語言中有種不錯的設計,即延遲(defer)語句,你可以在函式中新增多個defer語句。當函式執行到最後時,這些defer語句會按照逆序執行,最後該函式返回。特別是當你在進行一些開啟資源的操作時,遇到錯誤需要提前返回,在返回前你需要關閉相應的資源,不然很容易造成資源洩露等問題。如下程式碼所示,我們一般寫開啟一個資源是這樣操作的:

func ReadWrite() bool {
    file.Open("file")
// 做一些工作
    if failureX {
        file.Close()
        return false
    }
    if failureY {
        file.Close()
        return false
    }
    file.Close()
    return true
}

我們看到上面有很多重複的程式碼,Go的defer有效解決了這個問題。使用它後,不但程式碼量減少了很多,而且程式變得更優雅。在defer後指定的函式會在函式退出前呼叫。

func ReadWrite() bool {
    file.Open("file")
    defer file.Close()
    if failureX {
        return false
    }
    if failureY {
        return false
    }
    return true
}

如果有很多呼叫defer,那麼defer是採用後進先出模式,所以如下程式碼會輸出4 3 2 1 0

for i := 0; i < 5; i++ {
    defer fmt.Printf("%d ", i)
}

函式作為值、型別

在Go中函式也是一種變數,我們可以通過type來定義它,它的型別就是所有擁有相同的引數,相同的返回值的一種型別

type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

函式作為型別到底有什麼好處呢?那就是可以把這個型別的函式當做值來傳遞,請看下面的例子

package main
import "fmt"
type testInt func(int) bool // 宣告瞭一個函式型別
func isOdd(integer int) bool {
    if integer%2 == 0 {
        return false
    }
    return true
}
func isEven(integer int) bool {
    if integer%2 == 0 {
        return true
    }
    return false
}
// 宣告的函式型別在這個地方當做了一個引數
func filter(slice []int, f testInt) []int {
    var result []int
    for _, value := range slice {
        if f(value) {
            result = append(result, value)
        }
    }
    return result
}
func main(){
    slice := []int {1, 2, 3, 4, 5, 7}
    fmt.Println("slice = ", slice)
    odd := filter(slice, isOdd)    // 函式當做值來傳遞了
    fmt.Println("Odd elements of slice are: ", odd)
    even := filter(slice, isEven)  // 函式當做值來傳遞了
    fmt.Println("Even elements of slice are: ", even)
}

函式當做值和型別在我們寫一些通用介面的時候非常有用,通過上面例子我們看到testInt這個型別是一個函式型別,然後兩個filter函式的引數和返回值與testInt型別是一樣的,但是我們可以實現很多種的邏輯,這樣使得我們的程式變得非常的靈活。

Panic和Recover

Go沒有像Java那樣的異常機制,它不能丟擲異常,而是使用了panic和recover機制。一定要記住,你應當把它作為最後的手段來使用,也就是說,你的程式碼中應當沒有,或者很少有panic的東西。這是個強大的工具,請明智地使用它。那麼,我們應該如何使用它呢?

Panic

是一個內建函式,可以中斷原有的控制流程,進入一個令人恐慌的流程中。當函式F呼叫panic,函式F的執行被中斷,但是F中的延遲函式會正常執行,然後F返回到呼叫它的地方。在呼叫的地方,F的行為就像呼叫了panic。這一過程繼續向上,直到發生panic的goroutine中所有呼叫的函式返回,此時程式退出。恐慌可以直接呼叫panic產生。也可以由執行時錯誤產生,例如訪問越界的陣列。

Recover

是一個內建的函式,可以讓進入令人恐慌的流程中的goroutine恢復過來。recover僅在延遲函式中有效。在正常的執行過程中,呼叫recover會返回nil,並且沒有其它任何效果。如果當前的goroutine陷入恐慌,呼叫recover可以捕獲到panic的輸入值,並且恢復正常的執行。

下面這個函式演示瞭如何在過程中使用panic

var user = os.Getenv("USER")
func init() {
    if user == "" {
        panic("no value for $USER")
    }
}

下面這個函式檢查作為其引數的函式在執行時是否會產生panic:

func throwsPanic(f func()) (b bool) {
    defer func() {
        if x := recover(); x != nil {
            b = true
        }
    }()
    f() //執行函式f,如果f中出現了panic,那麼就可以恢復回來
    return
}

main函式和init函式

Go裡面有兩個保留的函式:init函式(能夠應用於所有的package)和main函式(只能應用於package main)。這兩個函式在定義時不能有任何的引數和返回值。雖然一個package裡面可以寫任意多個init函式,但這無論是對於可讀性還是以後的可維護性來說,我們都強烈建議使用者在一個package中每個檔案只寫一個init函式。

Go程式會自動呼叫init()和main(),所以你不需要在任何地方呼叫這兩個函式。每個package中的init函式都是可選的,但package main就必須包含一個main函式。

程式的初始化和執行都起始於main包。如果main包還匯入了其它的包,那麼就會在編譯時將它們依次匯入。有時一個包會被多個包同時匯入,那麼它只會被匯入一次(例如很多包可能都會用到fmt包,但它只會被匯入一次,因為沒有必要匯入多次)。當一個包被匯入時,如果該包還匯入了其它的包,那麼會先將其它包匯入進來,然後再對這些包中的包級常量和變數進行初始化,接著執行init函式(如果有的話),依次類推。等所有被匯入的包都載入完畢了,就會開始對main包中的包級常量和變數進行初始化,然後執行main包中的init函式(如果存在的話),最後執行main函式。下圖詳細地解釋了整個執行過程:

圖2.6 main函式引入包初始化流程圖

import

我們在寫Go程式碼的時候經常用到import這個命令用來匯入包檔案,而我們經常看到的方式參考如下:

import(
    "fmt"
)

然後我們程式碼裡面可以通過如下的方式呼叫

fmt.Println("hello world")

上面這個fmt是Go語言的標準庫,其實是去GOROOT環境變數指定目錄下去載入該模組,當然Go的import還支援如下兩種方式來載入自己寫的模組:

1.相對路徑

import “./model” //當前檔案同一目錄的model目錄,但是不建議這種方式來import

2.絕對路徑

import “shorturl/model” //載入gopath/src/shorturl/model模組

上面展示了一些import常用的幾種方式,但是還有一些特殊的import,讓很多新手很費解,下面我們來一一講解一下到底是怎麼一回事

點操作

我們有時候會看到如下的方式匯入包

import(
    . "fmt"
)

這個點操作的含義就是這個包匯入之後在你呼叫這個包的函式時,你可以省略字首的包名,也就是前面你呼叫的fmt.Println(“Hello World”)可以省略的寫成Println(“hello world”)

別名操作

別名操作顧名思義我們可以把包命名成另一個我們用起來容易記憶的名字

import(
    f "fmt"
)

別名操作的話呼叫包函式時字首變成了我們的字首,即f.Println(“hello world”)。

_操作

這個操作經常是讓很多人費解的一個操作符,請看下面這個import

import (
    "database/sql"
    _ "github.com/ziutek/mymysql/godrv"
)

_操作其實是引入該包,而不直接使用包裡面的函式,而是呼叫了該包裡面的init函式。

相關文章