博客 / 詳情

返回

go-lang之資源釋放/異常&錯誤處理

資源釋放-defer

類似於其他語言的 析構函數 比如PHP的 __destruct

func b() error {
    resp, err := http.Get("xxx.com")
    // 先判斷操作是否成功
    if err != nil {
        return err
    }
    // 如果操作成功,再進行Close操作
    defer resp.Body.Close()
    return nil
}

從上面的示例可看到,defer 是個延遲函數(延遲執行)

關於 defer 的一些常見誤區和陷阱

defer 語句是用來延遲執行函數的,且延遲發生在函數 return 之後
具體指: Go return 在底層等於: 返回值入棧, defer函數調用, return

func returnValues() int {
    var result int
    defer func() {
        // defer 匿名函數對變量的修改發生在 return 之後
        result++
        fmt.Println("defer")
    }()
    return result
}
// 如下輸出 0
fmt.Println(returnValues())

go允許多個defer同時存在
defer 執行順序遵循先進後出 first-in-last-out (FILO)
屬於棧操作,如下輸出:3、2、1

func stackingDefers() { 
    defer func() {
        fmt.Println("1")
    }()
    defer func() {
        fmt.Println("2")
    }()
    defer func() {
        fmt.Println("3")
    }()
}

函數被 deferred 時涉及的 參數變量值 在 defer 函數編寫時確定,而非實際調用時

func c() {
    i := 0
    defer fmt.Println(i) // 此時i變量值已被確定,即0
    i++
    defer fmt.Println(i) // 此時i++已執行,變量值已被確定為 1
}
// 輸出 1 0;因為defer的執行順序為 後進先出,所以先輸出 i++ 之後的 1;再輸出之前的 0
defer與閉包陷阱

先來看一個示例:

func main() {
    i := 1
    defer func() {
        fmt.Println("defer-閉包i值:", i)
    }()
    defer fmt.Println("defer-i值:", i)
    i += 100
}

結果輸出為:
defer-i值: 1
defer-閉包i值: 101

為啥捏?

  • 第二個defer,因為函數被 deferred 時涉及的 參數變量值 在 defer 函數編寫時已確定,所以輸出 defer-i值: 1
  • 第一個defer,閉包中的參數傳遞是引用類型傳遞,因此匿名函數中的 i 可以輸出 101(匿名函數都是閉包,按引用類型傳遞)
try-catch-finally

Go追求簡潔優雅,所以不支持傳統的 try-catch-finally 結構,因為Go語言的設計者們認為,將異常與控制結構混在一起很容易使得代碼變混亂(咱不懂,但內心大受震撼)。

如果要在go實現這麼個邏輯,需要用到這麼幾個關鍵詞:defer, panic, recover
如何使用 defer 執行 try-catch-finally 調試:

func main() {
    // 類似其他語言的 try (隱式包含)
    defer func() {
        // 捕獲異常:類似 catch(\Exception $e)
        if err := recover(); err != nil {
            // 輸出異常
            fmt.Println(err)
            // 打印錯誤堆棧: 類似於PHP的 debug_print_backtrace
            debug.PrintStack() // 輸出到日誌即可
            fmt.Println()
            fmt.Println("執行:planB!")
            planB()
        }
    }()
    // 注意!!用recover處理panic指令,defer 須在 panic 之前聲明,否則panic時會直接退出,recover無法捕獲到panic
    tryCatchFinally()
    fmt.Println("dadada...") // 這裏不會執行
}
func tryCatchFinally() {
    fmt.Println("begin")
    panic("拋出異常信息: throw new exception")
    fmt.Println("end") // 這裏不會執行
}
func planB() {
    fmt.Println("exec-planB, done!!")
}
關於內建函數 recover :

1、用來控制一個 goroutine 的 panicking 行為,捕獲panic,從而影響應用的行為
2、一般調用方式

  • 在defer函數中,通過 recover 來終止一個 gojroutine 的 panicking 過程,從而恢復正常邏輯的執行
  • 獲取通過panic傳遞的error,執行捕獲之後的邏輯,參考傳統tryCatchFinally

EOF - 錯誤處理

另一種常見錯誤處理,即 EOF(End-Of-File)文件結尾錯誤,可以直接捕獲,示例如下:

reader := strings.NewReader("clear is better than clever")
p := make([]byte, 5)
for {
    n, err := reader.Read(p)
    if err != nil {
        if err == io.EOF {
            fmt.Println("EOF:", n)
        } else {
            fmt.Println(err)
        }
        os.Exit(1)
    }
    // 注: p 每次循環都會被覆蓋,但如果最後一行不足5個字節時:
    // 僅會覆蓋最後一行獲取到的相應的字節數,比如最後一行3字節,僅覆蓋p的前3個,第4、5個字節維持上次循環時的賦值
    // 所以這裏輸出時使用了:p[:n]
    fmt.Println(n, string(p[:n]))
}

如上執行結果:

5 clear
5  is b
5 etter
5  than
5  clev
2 erlev
EOF: 0
exit status 1
user avatar tekin_cn 頭像 yinggaozhen 頭像
2 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.