panic/recover工作原理

themoonstone發表於2016-10-24

在 Golang 裡面沒有大家所熟悉的 try-cache 異常機制、而是用 panic/recover 代替 異常 throw / catch 和 panic / recover 是有些類似的,比如、recover 的作用是捕獲並返回 panic 提交的錯誤物件、這可以理解為通過呼叫 panic 丟擲一個值、該值可以通過呼叫 recover 函式進行捕獲。 主要的區別是,即使當前 goroutine 處於 panic 狀態,或當前 goroutine 中存在活動緊急情況,恢復呼叫仍可能無法檢索這些活動緊急情況丟擲的值。

For example:

package main

import (
    "fmt"
)

func main() {
    defer func() {
        defer func() {
            fmt.Println("6:", recover())
        }()
    }()
    defer func() {
        func() {
            fmt.Println("5:", recover())
        }()
    }()
    func() {
        defer func() {
            fmt.Println("1:", recover())
        }()
    }()
    func() {
        defer fmt.Println("2:", recover())
    }()
    func() {
        fmt.Println("3:", recover())
    }()

    fmt.Println("4:", recover())
    panic(1)
    defer func() {
        fmt.Println("0:", recover()) // never go here
    }()
}

上述程式中的 7 個恢復呼叫都沒有恢復程式。 程式崩潰並列印出堆疊跟蹤:

$ go run example1.go 1: <nil> 2: <nil> 3: <nil> 4: <nil> 5: <nil> 6: <nil> panic: 1

goroutine 1 [running]: ... 顯然,第 0 個恢復呼叫是不可達的。 對於其他 recover,讓我們先檢查 Go 規範: 如果滿足以下任一條件,recover 的返回值為 nil:

  • 1.panic 引數是 nil
  • 2.當前 goroutine 沒有產生 panic
  • 3.recover 不是由延遲函式直接呼叫。

讓我們忽略第一個條件。 第二個條件覆蓋第一/第二/第三和第四 recover 呼叫。 第三個覆蓋第 5 個 recover 呼叫。 然而,三個條件中沒有一個覆蓋第六個 recover 呼叫。 怎樣才能讓 recover 呼叫起作用?如下 :

import(&quot;fmt&quot;)
func main(){
    defer func(){
    fmt.Println(recover())// 1
    }()
    panic(1)
}

現在,panic 值被 recover 呼叫捕獲,並且程式不會崩潰。 那麼,使 recover 呼叫生效的主要規則是什麼? 首先,讓我們學習一些概念和說明。

概念:延遲函式呼叫概念:延遲函式呼叫

當函式被延遲呼叫時,或者函式的呼叫使用 defer 關鍵字作為字首,則呼叫被稱為延遲呼叫。

package main
func main(){
    defer func(){// deferred function calling
                func(){// not a deferred function calling
                defer recover()// deferred function calling
        }()
    }()
    func(){
    // not a deferred function calling
        defer func(){
    /// deferred function calling
            }()
    }()
}

概念:函式呼叫級別和 Goroutine 執行級別

函式呼叫級別是指函式呼叫的深度,與主函式或者 goroutine 的入口函式相關。

package main

funcmain(){
// level 0  
    go func(){
    // level 0
        func(){
        // level 1
        }()
        func(){
        // level 1          
            func(){
            // level 2
            }()
        }()
    }()
    func(){
    // level 1
        func(){
        // level 2
            go func(){
            // level 0
            }()
        }()
        go func(){
        // level 0
        }()
    }()
}

goroutine 當前執行點的呼叫級別稱為 goroutine 的執行級別。

說明:panic 只能向上傳播

是的,panic 只能向上傳播,沿著函式呼叫堆疊反向。panic 從不通過深入到函式呼叫中傳播。

package main

import&quot;fmt&quot;
func main(){// calling level 0
    defer func(){// calling level 1
        fmt.Println(&quot;Now, the panic is still in calling level 0&quot;)
        func(){// calling level 2
            fmt.Println(&quot;Now, the panic is still in calling level 0&quot;)
            func(){// calling level 3
                fmt.Println(&quot;Now, the panic is still in calling level 0&quot;)
            }()
        }()
    }()
    defer fmt.Println(&quot;Now, the panic is in calling level 0&quot;)
    func(){
        // calling level 1
        defer fmt.Println(&quot;Now, the panic is in calling level 1&quot;)
        func(){// calling level 2
            defer fmt.Println(&quot;Now, the panic is in calling level 2&quot;)
            func(){// calling level 3
                defer fmt.Println(&quot;Now, the panic is in calling level 3&quot;)
                panic(1)
            }()
        }()
    }()
}

The output: Now, the panic is in calling level 3

Now, the panic is in calling level 2

Now, the panic is in calling level 1

Now, the panic is in calling level 0

Now, the panic is still in calling level 0

Now, the panic is still in calling level 0

Now, the panic is still in calling level 0

panic: 1

goroutine 1 [running]:

...

概念:panic 級別

panic 級別意味著 panic 傳播到函式呼叫的哪一層級、因為 panic 只能向上傳遞、所以 panic 等級永遠不加增加、只會減小、在 goroutine 中,當前 panic 的水平永遠不會大於 goroutine 的執行水平。

說明:在同一層級上、新的 panics 會壓制原有 panics

Example:
package main

import&quot;fmt&quot;
func main(){
    defer fmt.Println(&quot;program will not crash&quot;)
    defer func(){
        fmt.Println(recover())// 3
    }()
    defer fmt.Println(&quot;now, panic 3 suppresses panic 2&quot;)
    defer panic(3)
    defer fmt.Println(&quot;now, panic 2 suppresses panic 1&quot;)
    defer panic(2)
    panic(1)
}

Outputs:

now, panic 2 suppresses panic 1

now, panic 3 suppresses panic 2

3

program will not crash

在以上程式中、我們最終只能看到一個 panic 3、panic 3 被 recover 捕獲、因此程式不會崩潰 在一個 goroutine 中,在任何時候在相同的呼叫級別將有至多一個主動 panic。 特別是,當執行點執行在 goroutine 的呼叫級別 0 時,在 goroutine 中最多隻有一個活動的 panic。

說明:多個主動 panic 在一個 Goroutine 中的共存

Example:

package main

import&quot;fmt&quot;
func main(){// callnig level 0
    defer fmt.Println(&quot;program will crash, for panic 3 is stll active&quot;)
    defer func(){// calling level 1
        defer func(){// calling level 2
            fmt.Println(recover())// 6
        }()// the level of panic 3 is 0.// the level of panic 6 is 1.
        defer fmt.Println(&quot;now, there are two active panics: 3 and 6&quot;)
        defer panic(6)// will suppress panic 5
        defer panic(5)// will suppress panic 4
        panic(4)// will not suppress panic 3, for they have differrent levels 
        // the  level of panic 3 is 0.// the level of panic 4 is 1.
    }()
    defer fmt.Println(&quot;now, only panic 3 is active&quot;)
    defer panic(3)// will suppress panic 2
    defer panic(2)// will suppress panic 1
    panic(1)
}

在該示例中,panic 6、兩個處於 active 狀態中的 panic 之一被 recover 捕獲。 但是其他 panic,panic 3,在主呼叫結束時仍然活動,所以程式將崩潰。 Outputs:

now, only panic 3 is active

now, there are two active

panics: 3 and 6

6

program will crash, for panic 3 is stll active

panic: 1

panic: 2

panic: 3

goroutine 1 [running]: ...

說明:低階的 panics 將會被首先捕獲

Example:

package main

import&quot;fmt&quot;
func main(){
    defer func(){
        defer func(){
            fmt.Println(&quot;panic&quot;,recover(),&quot;is recovered&quot;)// panic 2 is recovered
        }()
        defer fmt.Println(&quot;panic&quot;,recover(),&quot;is recovered&quot;)// panic 1 is recovered
        defer fmt.Println(&quot;now, two active panics coexist&quot;)
        panic(2)
    }()
    panic(1)
}

Outputs:

now, two active panics coexist

panic 1 is recovered

panic 2 is recovered

那麼,什麼是使 recover 呼叫生效的主要規則是什麼? 規則很簡單

在一個 goroutine 中,如果 recover 呼叫的呼叫函式是 F 並且 F 呼叫的級別是 L,則為了使 recover 呼叫生效,F 呼叫必須是延遲呼叫,並且必須存在主動 panic 的水平為 L-1。 相對來說、這是一個比 go 規格更好的描述、 現在你可以回頁面檢查為什麼第一個例子中的第 6 個 recover 呼叫不會生效了

原文連結:http://www.tapirgames.com/blog/golang-panic-recover-mechanism

更多原創文章乾貨分享,請關注公眾號
  • panic/recover工作原理
  • 加微信實戰群請加微信(註明:實戰群):gocnio

相關文章