Golang panic 用法

Golang panic 用法

Go 語言追求簡潔優雅,所以,Go 語言不支持傳統的 try…catch…finally 這種異常,因爲 Go 語言的設計者們認爲,將異常與控制結構混在一起會很容易使得代碼變得混亂。因爲開發者很容易濫用異常,甚至一個小小的錯誤都拋出一個異常。在 Go 語言中,使用多值返回來返回錯誤。不要用異常代替錯誤,更不要用來控制流程。在極個別的情況下,也就是說,遇到真正的異常的情況下(比如除數爲 0 了)。才使用 Go 中引入的 Exception 處理:defer, panic, recover。

這幾個異常的使用場景可以這麼簡單描述:Go 中可以拋出一個 panic 的異常,然後在 defer 中通過 recover 捕獲這個異常,然後正常處理。

package main

import "fmt"

func main(){
    fmt.Println("c")
     defer func(){ // 必須要先聲明defer,否則不能捕獲到panic異常
        fmt.Println("d")
        if err:=recover();err!=nil{
            fmt.Println(err) // 這裏的err其實就是panic傳入的內容,55
        }
        fmt.Println("e")
    }()

    f() //開始調用f
    fmt.Println("f") //這裏開始下面代碼不會再執行
}

func f(){
    fmt.Println("a")
    panic("異常信息")
    fmt.Println("b") //這裏開始下面代碼不會再執行
    fmt.Println("f")
}
輸出結果:

c
a
d
異常信息
e
比如panic函數內有:
    defer   函數1
    defer   函數2
    defer   函數3
那麼執行順序就是:
    函數3
    函數2
    函數1

recover:

注意:利用 recover 處理 panic 指令,defer 必須在 panic 之前聲明,否則當 panic 時,recover 無法捕獲到 panic.

應用示例:

比如 go 編譯器的源碼

註釋: 編譯器是構建正在運行的二進制文件的編譯器工具鏈的名稱。已知的工具鏈爲:
gc Also known as cmd/compile.
gccgo The gccgo front end, part of the GCC compiler suite.

介紹 錯誤和異常 (errors and exceptions)

大多數編程語言支持 exception 作爲處理 error 的標準方式,比如 Java,Python。雖然方便,但也會帶來許多問題,這就是爲什麼他們不喜歡其他語言或者風格。對 exception 的主要吐槽點是它們爲控制流引入了 "side channel",當閱讀代碼的時候,你必須時刻記住這個 exception 引發的流程控制方向。這也導致某些代碼閱讀起來比較困難 [1]。

讓我們開始具體談談 Go 中的錯誤處理。我假定你知道 Go 中錯誤處理的 “標準” 方式。下面如何打開文件的代碼:

f, err := os.Open("file.txt")
if err != nil {
    // handle error here
}
// do stuff with f here

如果文件不存在, os.Open() 函數將返回一個非空 error ,在其他語言中這樣的錯誤處理是完全不同的,比如 Python 中的內建 open() 函數將在錯誤發生時拋出異常。

try:
    with open("file.txt") as f:
        # do stuff with f here
except OSError as err:
    # handle exception err here

Python 始終堅持通過 exception 來處理 error。因爲這種無處不在的錯誤處理方式導致經常被吐槽。甚至利用 exception 作爲序列結束的信號。到底 exception 的真正含義是什麼?以下是來自 Rob Pike 在郵件中 對此的貼切闡述,其中塑造了現有的 Go panic/recover 機制雛形。

這正是提案試圖避免的那種事情。Panic 和 recover 不是通常意義的異常機制。通常的方式是將 exception 和一個控制結構相關聯,鼓勵細粒度的 exception 處理,導致代碼往往不易閱讀。在 error 和調用一個 panic 之間確實存在差異,而且我們希望這個差異很重要。在 Java 中打開一個文件會拋出異常。在我的經驗中,打開文件失敗是最平常不過的事。而且還需要我寫許多代碼來處理這樣的 exception。

客觀的講。exception 的支持者嘲笑 Go 的這種過於明確的 error 處理有多方面的原因。首先,請注意上面兩個例子中代碼的順序。在 Python 中,程序的主要流程緊跟在 open 調用之後,並且錯誤處理被委託給後一階段(更不用說在許多情況下,異常將被堆棧中更上一級的函數捕獲到而不是在此函數中)。另一方面,在 Go 中,立刻處理錯誤這種方式,可能會使主程序流程混淆。此外,Go 的錯誤處理非常冗長 - 這是該語言的主要吐槽點之一。我將在後面提到一種可能的方法來解決這個問題。

除了上面 Rob 的引用之外,在 FAQ 中總結了 Go 的 exception 哲學。

我們認爲將異常耦合到控制結構(如 try-catch-finally 慣用語)會導致代碼錯綜複雜。它還傾向於鼓勵程序員標記太多普通錯誤,例如打開文件失敗。

然而,在某些情況下,具有類似異常的機制實際上是有用的 ; 像 Go 這樣的高級語言甚至是必不可少的。這就是存在 panic 和 recover 的原因。

偶爾的 panic 是必要的

Go 是一種安全的語言,運行時檢查一些嚴重的編程錯誤。例如在你訪問超出 slice 邊界的元素時,這種行爲是未定義的,因此 Go 會在運行時 panic。例如下面的小程序。

package main

import (
    "fmt"
)

func main() {
    s := make([]string, 3)
    fmt.Println(s[5])
}

程序將終止於一個運行時 error。

panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
    /tmp/sandbox209906601/main.go:9 +0x40

其他一些會引發 panic 的就是通過值爲 nil 的指針訪問結構體的字段,關閉已經關閉的 channel 等。怎樣選擇性的 panic ?可以通過訪問 slice 時返回 result,error 兩個值的方式實現。也可以將 slice 的元素賦值給一個可能返回 error 的函數,但是這樣會將代碼變複雜。想象一下,寫一個小片段,foo,bar,baz 都只是一個字符串的一個 slice,實現片段之間的拼接。

foo[i] = bar[i] + baz[i]

就會變成下面這樣冗長的代碼:

br, err := bar[i]
if err != nil {
    return err
}
bz, err := baz[i]
if err != nil {
    return err
}
err := assign_slice_element(foo, i, br + bz)
if err != nil {
    return err
 }

這不是開玩笑,不同語言處理這樣的方式是不一樣的。如果 slices/lists/arrays 的指針 i 越界了,在 Python 和 Java 中就會拋出異常。C 中沒有越界檢查,所以你就可以盡情的蹂躪邊界外的內存空間,最後將導致程序崩潰或者暴露安全漏洞。C++ 中將採用折中的處理方式。性能優先的模塊採用這種不安全的 C 模式,其他模塊(比如 std::vector::at)採用拋出異常的方式。

因爲上面重寫的小片段變得如此冗長是不可接受的。Go 選擇了 panic ,這是一種類似異常的機制,在代碼中保留了像 bugs 這樣最原始的異常條件。

這不只是內建代碼能夠這樣用,自定義代碼也可以在任何需要的地方調用 panic。在有些可能導致可怕錯誤的地方還鼓勵使用 panic 拋出 error,比如 bug 或者一些關鍵因素被違反的時候。比如在 swich 的某個 case 在當前上下文中是不可能發生的,在這種 case 中只有一個 panic 函數。這無形中等價於 Python 中的 raise 或者 C++ 中的 throw。這也強有力的證明了在捕獲異常方面 Go 的異常處理的特殊之處。

轉自:

zhuanlan.zhihu.com/p/373653492

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/Rp2cgPsDOnM1gvgdR9louQ