1. 程式人生 > >Go語言學習筆記9

Go語言學習筆記9

5.Go語言流程控制方法

5.2 defer語句

defer 語句被用於預定對一個函式的呼叫。被 defer 語句呼叫的函式稱為延遲函式。defer 語句只能出現在函式或方法的內部。

一條 defer 語句總是以關鍵字 defer 開始。在 defer 的右邊還會有一條表示式語句,且它們之間要以空格分隔。例如:

defer fmt.Println("The finishing touches.")

如上的表示式語句必須代表一個函式或方法的呼叫。但是像針對各種內建函式的那些呼叫表示式,因為它們並不能稱為表示式語句,所以不允許出現在這裡。同時這個位置出現的表示式語句是不能被圓括號括起來的。

defer 語句的執行時機總是在直接包含它的那個函式(簡稱外圍函式)把流程控制權交還給它的呼叫方的前一刻,無論 defer 語句出現在外圍函式的函式體中的哪一個位置上。具體分為:

  • 當外圍函式的函式體中的相應語句全部被正常執行完畢的時候,只有在該函式中的所有 defer 語句都被執行完畢之後該函式才會真正地結束執行。

  • 當外圍函式的函式體中的 return 語句被執行的時候,只有在該函式中的所有 defer 語句都被執行完畢之後該函式才會真正地返回。

  • 當在外圍函式中有執行時恐慌發生的時候,只有在該函式中的所有 defer 語句都被執行完畢之後該執行時恐慌才會真正地被擴散至該函式的呼叫方。

也就是說,外圍函式的執行的結束會由於其中的 defer 語句的執行而被推遲。例如:

func isPositiveEnenNumber(number int) (result bool){
    defer fmt.Println("done.");
    if number < 0 {
        panic(errors.New("The number is a negative number!"))
    }
    if number % 2 ==0 {
        return true
    }
    return
}

使用 defer 語句的優勢有兩個:

  • 收尾任務總會被執行,這樣就不會因粗心大意而造成資源的浪費。

  • 可以把它們放到外圍函式的函式體中的任何地方(一般是函式體開始處或緊跟在申請資源的語句的後面),而不是隻能放在函式體的最後。

defer 語句中,呼叫的函式不但可以是已宣告的命名函式,還可以是臨時編寫的匿名函式。例如:

defer func(){
    fmt.Println("The finishing touches.")
}()

注意:一個針對匿名函式的呼叫表示式是由一個函式字面量和一個代表了呼叫操作的一對圓括號組成的。

無論在defer關鍵字右邊的是命名函式還是匿名函式,都可以稱為延遲函式。因為它總是會被延遲到外圍函式執行結束前一刻才被真正地呼叫。每當 defer 語句被執行的時候,傳遞給延遲函式的引數都會以通常的方式被求值。如下:

func begin(funcName string) string {
    fmt.Printf("Enter function %s.\n", funcName)
    return funcName
}

func end(funcName string) string {
    fmt.Printf("Exit function %s.\n", funcName)
    return funcName
}

func record(){
    defer end(begin("record"))
    fmt.Println("In function record")
}

對函式 record 進行呼叫之後,執行截圖如下:

這裡寫圖片描述

出於同一條 defer 語句可能會被多次執行的考慮,如下:

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

對函式 printNumbers 進行呼叫之後,執行截圖如下:

這裡寫圖片描述

如上的函式 printNumbers 有兩個細節:

  • 在for語句的每次迭代的過程中都會執行一次其中的defer語句。Go語言會把代入引數值之後的呼叫表示式另行儲存,以此類推,後面幾次迭代所產生的延遲函式呼叫表示式依次為:

    fmt.Printf("%d ", 0)
    fmt.Printf("%d ", 1)
    fmt.Printf("%d ", 2)
    fmt.Printf("%d ", 3)
    fmt.Printf("%d ", 4)
  • 對延遲函式呼叫表示式的求值順序是與它們所在的defer語句被執行的順序完全相反的。每當Go語言把已帶入引數值的延遲函式呼叫表示式另行儲存之後,還會把它追加到一個專門為當前外圍函式儲存延遲函式呼叫表示式的列表(也就是棧)當中,而該列表總是先進後出。因此這些延遲函式呼叫表示式的求值順序會是:

    fmt.Printf("%d ", 4)
    fmt.Printf("%d ", 3)
    fmt.Printf("%d ", 2)
    fmt.Printf("%d ", 1)
    fmt.Printf("%d ", 0)

再看看下面的例子,如下:

func appendNumber(ints []int) (result []int) {
    result = append(ints, 1)
    defer func(){
        result = append(result, 2)
    }()
    result = append(result, 3)
    defer func(){
        result = append(result, 4)
    }()
    result = append(result, 5)
    defer func(){
        result = append(result, 6)
    }()
    return result
}
func main(){
    res := appendNumber([]int{0})
    fmt.Printf("result: %v\n", res)
}

執行結果截圖如下:

這裡寫圖片描述

現在考慮一個問題,把 printNumbers 函式的宣告修改為如下:

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

執行結果截圖如下:

這裡寫圖片描述

現在對執行結果分析可知:在 for 語句被執行完畢的時候,共有 5 個相同的延遲函式呼叫表示式被儲存在專屬列表()中,例如:

func() {
    fmt.Printf("%d ", i)
}()

這時的變數 i 已經被修改為了 5,對 5 個相同的呼叫表示式的求值都會使標準輸出打印出 5

針對上面的情況,可以修改如下:

defer func(i int) {
    fmt.Printf("%d ", i)
}(i)//在defer語句被執行的時候,傳遞給延遲函式的這個引數i就會被求值。

執行結果截圖如下(這個和第一個版本的 printNumbers 函式執行效果是相同的):

這裡寫圖片描述

如果延遲函式是一個匿名函式,並且在外圍函式的宣告中存在命名的結果宣告,那麼在延遲函式中的程式碼使可以對命名結果的值進行訪問和修改的。例如:

func modify(n int) (number int) {
    defer func(){
        number += n
    }()
    number++
    return
}

延遲函式的宣告中可以包含結果宣告,但是其返回的結果值會在它被執行完畢時被丟棄。因此在編寫延遲函式的宣告的時候不會為其新增結果宣告。另外,推薦以傳參的方式提供延遲函式所需的外部值。例如:

//傳入引數為1時,modify函式的結果值是5
func modify(n int) (number int) {
    defer func(plus int) (result int){
        result = n + plus
        number += result
        return //此處雖然返回了結果,但是卻並不會產生任何效果。
    }(3)//延遲函式呼叫時直接傳外部引數
    number++
    return
}

5.3異常處理

在前面的博文中已經涉及了Go語言的異常處理的知識,比如介面型別error內建函式panic標準庫程式碼包errors。本小節將對Go語言的各種異常處理方法進行系統的講解。

1.error

在Go語言標準庫程式碼包中的很多函式和方法會返回 error 型別值來表明錯誤狀態及其詳細資訊。error 是一個預定義識別符號,它代表了一個Go語言內建的介面型別。該介面型別宣告如下:

type error interface {
    Error() string
}

其中,Error 方法宣告額意義就在於為方法呼叫方提供當前錯誤狀態的詳細資訊。任何資料型別只要實現了這個可以返回 string 型別值的 Error 方法就可以成為一個 error 介面型別的實現。但在通常情況下,不需要自己編寫一個 error 的實現型別,Go語言的標準庫程式碼包 errors 為我們提供了一個用於建立 error 型別值的函式 New,宣告如下:

func New(text string) error {
    return &errorString(text)//返回一個error型別值,它的動態型別就是errors.errorString型別
}

errorString 的首字母可知,errors.errorString型別是一個包級私有的型別。它只是errors包的內部實現的一部分,而非公開的APIerrors.errorString 型別及其方法的宣告如下:

type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

列印error型別值所代表的錯誤的詳細資訊。

var err error = errors.New("A normal error example")
fmt.Println(err)
fmt.Printf("%s\n", err)

另一個可以生成 error 型別值的方法是呼叫 fmt 包中的 Errorf 函式,呼叫類似如下程式碼:

err2 := fmt.Errorf("%s\n","A normal error")//初始化一個error型別值並作為該函式的結果值返回給呼叫方。

在fmt.Errorf函式的內部,建立和初始化error型別值的操作正是通過呼叫errors.New函式來完成的。

結構體型別 os.PathError 是一個 error 介面型別的實現型別。宣告及其方法如下:

// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op  string  // “open” , ”unlink”, etc
    Path string // The associated file
    Err  error  // Returned by the system call
}

func (e *PathError) Error() string { 
    return e.Op + " " + e.Path + ": " + e.Err.Error() 
}

先判定獲取到的 error 型別值的動態型別,再依次來進行必要的型別轉換和後續操作。例如:

file , err := os.Open("E:\\Software\\lgh.txt")
if err != nil {
    if pe, ok := err.(*os.PathError); ok {// 判斷err是否為*os.PathError型別
        fmt.Printf("Path Error: %s \n(op=%s,path=%s)\n", pe.Err, pe.Op, pe.Path)    
    } else {
        fmt.Printf("Unknown Error: %s\n",err)
    }
}

如上Open的引數的檔案路徑不存在,執行截圖如下:

這裡寫圖片描述

在上面示例中的 os.Open 函式在執行過程中沒有發生任何錯誤,就可以對變數 file 的內容進行讀取了。例如:

reader := bufio.NewReader(file)//建立一個可以讀取檔案內容的讀取器
var buf bytes.Buffer // 快取從檔案讀取出來的內容
for {
    //reader讀取器,返回3個結果值。reader型別所屬的方法如下:
    //func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error)
    //當讀取器讀到file所代表的檔案的末尾時,ReadLine方法會直接將變數io.EOF的值作為它的第三個結果值err返回。
    byteArray, _, err1 := reader.ReadLine()
    if err1 != nil {
        //io.EOF是error型別的變數,在標準庫程式碼包io中,它的宣告如下:
        //var EOF = errors.New("EOF") ,EOF是檔案結束符(End Of File)的縮寫。
        //嚴格來說,EOF並不應該算作一個真正的錯誤,而僅僅屬於一種"錯誤訊號"
        if err1 == io.EOF {//判斷讀取器是否已經讀到了檔案的末尾
            break
        } else {
            fmt.Printf("Read Error: %s\n", err1)
            break
        }
    } else {
        buf.Write(byteArray)
    }
    fmt.Printf("%s\n", byteArray)
}

實現 error 介面型別的另一個技巧是,可以通過把 error 介面型別嵌入到新的介面型別中來對它進行擴充套件。標準庫程式碼包 net 中的 Error 介面型別,宣告如下:

//An Error represents a network error
type Error interface {
    error
    Timeout() bool // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

一些在 net 包中宣告的函式會返回動態型別為 net.Errorerror 型別值。如果變數 err 的動態型別是 net.Error,可以根據它的 Temporary 方法的結果值來判斷當前的錯誤狀態是否臨時的:

if netErr, ok := err.(net.Error); ok && netErr.Temporary(){
    // 省略若干語句
}

如果是臨時的,那麼就可以間隔一段時間之後再對之前的操作進行重試,否則就記錄錯誤狀態的資訊並退出。

2.panic

Go語言內建的一個專用函式,目的使程式設計人員能夠在自己的程式中報告執行期間的,不可恢復的錯誤狀態。panic 函式被用於停止當前的控制流程的執行並報告一個執行時恐慌。它可以接受一個任意型別的引數值,這個引數常常是一個 string 型別值或者 error 型別值。例如:

package main

import (
    "errors"
)

func main(){
    outerFunc()
}

func outerFunc(){
    innerFunc()
}

func innerFunc(){
    panic(errors.New("A intended fatal error!"))
}

當在函式 innerFunc 中呼叫 panic 函式之後,函式 innerFunc 的執行會被停止。然後,流程控制權會被交回給函式 innerFunc 的呼叫方 outerFunc 函式,此時,outerFunc 函式的執行也將被停止。執行時恐慌就這樣沿著呼叫棧反方向進行傳達,直至到達當前 Goroutine(也被稱為Go程,可以看作是一個能夠獨佔一個系統執行緒並在其中執行程式的獨立環境)呼叫棧的最頂層。這時,當前 Goroutine 的呼叫棧中的所有函式的執行都已經被停止了,意味著程式已經崩潰了。

執行時恐慌並不都是通過呼叫 panic 函式的方式引發的。它也可以由Go語言的執行時系統來引發。例如:

myIndex := 4
ia := [3]int{1, 2, 3}
_ = ia[myIndex]//產生了一個數組訪問越界的執行時錯誤,會引發一個執行時恐慌。

如上這個執行時恐慌由執行時系統報告的,它相當於顯示地呼叫 panic 函式並傳入一個 runtime.Error 型別的引數值,該型別的宣告如下:

type Error interface {
    error 
    // RuntimeError is a no-op function but serves to distinguish types that are runtime errors
    // from ordinary errors: a type is a runtime error if it has a RuntimeError method.
    RuntimeError()
}

3.recover

執行時恐慌一旦被引發就會向呼叫方傳遞直至程式崩潰。Go語言提供了專用於“攔截”執行時恐慌的內建函式—recover。它可以使當前的程式從執行時恐慌的狀態中恢復並重新獲得流程控制權。recover 函式有一個 interface{} 型別的結果值,如果當前的程式正處於執行時恐慌的狀態下,那麼呼叫 recover 函式將會得到一個非nilinterface{} 型別值。如果當時的執行時恐慌是由Go語言的執行時程式引發的,就會獲得一個 runtime.Error 型別的值。

只有在 defer 語句的延遲函式中呼叫 recover 函式才能夠起到“攔截”執行時恐慌的作用。例如:

defer func(){
    if r := recover(); r != nil {
        fmt.Printf("Recovered panic: %s\n", r)
    }
}()

再看一個命令原始碼檔案,有助於理解panic函式、recover函式和defer語句有關的執行機制。例如:

package main

import (
    "fmt"
)

func main(){
    fetchDemo()
    //由於執行時恐慌在將要被繼續傳遞給fetchDemo函式的呼叫方的時候被“攔截”。
    //因此fetchDemo函式的呼叫方(也就是main函式)得以重獲流程控制權,下一條語句可以列印
    fmt.Println("The main function is executed.") 
}

func fetchDemo() {
    defer func() {
        if v := recover(); v != nil {
            fmt.Printf("Recovered a panic. [index=%d]\n", v)
        }
    }()
    ss := []string{"A", "B", "C"}
    fmt.Printf("Fetch the elements in %v one by one...\n", ss)
    fetchElement(ss, 0)
    fmt.Println("The elements fetching is done.")//上面的語句出現了執行時恐慌,因此不會執行
}

func fetchElement(ss []string, index int) (element string) {
    if index >= len(ss) {
        fmt.Printf("Occur a panic![index=%d]\n", index)
        panic(index)
    }
    fmt.Printf("Fetching the element... [index=%d]\n", index)
    element = ss[index]
    defer fmt.Printf("The elements is \"%s\". [index=%d]\n", element, index)
    fetchElement(ss, index + 1)
    return
}

如上命令原始碼檔案執行結果截圖:

這裡寫圖片描述

在Go語言標準庫中,即使使用的某個程式實體的內部發生了執行時恐慌,這個執行時恐慌也會在被傳遞給我們編寫的程式使用方之前被“平息”並以 error 型別值的形式返回給使用方。在這些標準庫程式碼包中,往往都會有自己的 error 介面型別的實現。只有當呼叫 recover 函式得到的結果值的型別是它們自定義的 error 型別的實現型別的時候,才會去處理這個執行時恐慌,否則就會重新引發(官方使用的詞彙是 re-panic)一個執行時恐慌並攜帶相同的值。

在標準庫程式碼包 fmtscan.goToken 函式就是如下的這樣處理執行時恐慌的。宣告如下:

func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {
    defer func() {
        if e := recover(); e != nil {
            if se, ok := e.(scanError); ok {
                err = se.err
            } else {
                panic(e)
            }
        }
    }()
    // 省略若干條語句
}

Token 函式包含的延遲函式中,當執行時恐慌攜帶的值的型別是 fmt.scanError 型別的時候,這個值就會被賦值給代表結果值的變數 err,否則執行時恐慌就會被重新引發。

一個執行時恐慌無論重新引發幾次,它所有的引發資訊都依然會被提供在最終的程式崩潰報告中。重新引發一個執行時恐慌的時候使用如下:

panic(e)

在使用Go語言編寫程式時,在使用上面類似 Token 函式的慣用法之前應該明確和統一可以被立即處理和需要被重新引發的執行時恐慌的種類。一般情況下,如果攜帶的值是動態型別為 runtime.Errorerror 型別值的話,這個執行時恐慌就應該被重新引發。從執行時恐慌的分類和處理決策角度看,在必要時自行定義一些 error 型別的實現型別是有好處的。

建議:對於執行時恐慌的引發,應該在遇到致命的、不可恢復的錯誤狀態時才去引發一個執行時恐慌,否則,可以完全利用函式或方法的結果值來向程式使用方傳達錯誤狀態。另外,應該僅在程式模組的邊界位置上的函式或方法中對執行時恐慌進行“攔截”和“平息”。

本篇講述了Go語言特殊流程控制方法 defer 和異常處理時涉及的errorpanicrecover

最後附上知名的Go語言開源框架(每篇更新一個):

Gobot: 一個非常有意思的開源專案。它旨在成為下一代自動機工程學框架。換句話說,我們可以用它來控制機器人!它已經支援了10個(或者更多)不同的硬體平臺。這其中包括已經被國內的計算機硬體發燒友所熟知的 Arduino 。該開源專案的官方網址是http://gobot.io