天天看點

Golang中panic 和 recover

什麼是 panic?

在 Go 語言中,程式中一般是使用​​錯誤​​來處理異常情況。對于程式中出現的大部分異常情況,錯誤就已經夠用了。

當程式發生 panic 時,使用 ​

​recover​

​ 可以重新獲得對該程式的控制。

可以認為 ​

​panic​

​​ 和 ​

​recover​

​​ 與其他語言中的 ​

​try-catch-finally​

​​ 語句類似,隻不過一般我們很少使用 ​

​panic​

​​ 和 ​

​recover​

​​。而當我們使用了 ​

​panic​

​​ 和 ​

​recover​

​​ 時,也會比 ​

​try-catch-finally​

​ 更加優雅,代碼更加整潔。

什麼時候應該使用 panic?

需要注意的是,你應該盡可能地使用​​錯誤​​,而不是使用 panic 和 recover。隻有當程式不能繼續運作的時候,才應該使用 panic 和 recover 機制。

panic 有兩個合理的用例。

  1. 發生了一個不能恢複的錯誤,此時程式不能繼續運作。 一個例子就是 web 伺服器無法綁定所要求的端口。在這種情況下,就應該使用 panic,因為如果不能綁定端口,啥也做不了。
  2. 發生了一個程式設計上的錯誤。 假如我們有一個接收指針參數的方法,而其他人使用 ​

    ​nil​

    ​ 作為參數調用了它。在這種情況下,我們可以使用 panic,因為這是一個程式設計錯誤:用 ​

    ​nil​

    ​ 參數調用了一個隻能接收合法指針的方法。

panic 示例

func panic(interface{})      

我們會寫一個例子,來展示 ​

​panic​

​ 如何工作。

package main

import (  
    "fmt"
)

func fullName(firstName *string, lastName *string) {  
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}      

​​在 playground 上運作​​

運作該程式,會有如下輸出:

panic: runtime error: last name cannot be nil

goroutine 1 [running]:  
main.fullName(0x1040c128, 0x0)  
    /tmp/sandbox135038844/main.go:12 +0x120
main.main()  
    /tmp/sandbox135038844/main.go:20 +0x80      

我們來分析這個輸出,了解一下 panic 是如何工作的,并且思考當程式發生 panic 時,會怎樣列印堆棧跟蹤。

在第 19 行,我們将 ​

​Elon​

​​ 指派給了 ​

​firstName​

​​。在第 20 行,我們調用了 ​

​fullName​

​​ 函數,其中 ​

​lastName​

​​ 等于 ​

​nil​

​​。是以,滿足了第 11 行的條件,程式發生 panic。當出現了 panic 時,程式就會終止運作,列印出傳入 panic 的參數,接着列印出堆棧跟蹤。是以,第 14 行和第 15 行的代碼并不會在發生 panic 之後執行。程式首先會列印出傳入 ​

​panic​

​ 函數的資訊:

panic: runtime error: last name cannot be empty      

接着列印出堆棧跟蹤。

程式在 ​

​fullName​

​ 函數的第 12 行發生 panic,是以,首先會列印出如下所示的輸出。

main.fullName(0x1040c128, 0x0)  
    /tmp/sandbox135038844/main.go:12 +0x120      

接着會列印出堆棧的下一項。在本例中,堆棧跟蹤中的下一項是第 20 行(因為發生 panic 的 ​

​fullName​

​ 調用就在這一行),是以接下來會列印出:

main.main()  
    /tmp/sandbox135038844/main.go:20 +0x80      

現在我們已經到達了導緻 panic 的頂層函數,這裡沒有更多的層級,是以結束列印。

發生 panic 時的 defer

我們重新總結一下 panic 做了什麼。當函數發生 panic 時,它會終止運作,在執行完所有的延遲函數後,程式控制傳回到該函數的調用方。這樣的過程會一直持續下去,直到目前協程的所有函數都傳回退出,然後程式會列印出 panic 資訊,接着列印出堆棧跟蹤,最後程式終止。

在上面的例子中,我們沒有延遲調用任何函數。如果有延遲函數,會先調用它,然後程式控制傳回到函數調用方。

我們來修改上面的示例,使用一個延遲語句。

package main

import (  
    "fmt"
)

func fullName(firstName *string, lastName *string) {  
    defer fmt.Println("deferred call in fullName")
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}      

​​在 playground 上運作​​

上述代碼中,我們隻修改了兩處,分别在第 8 行和第 20 行添加了延遲函數的調用。

該函數會列印:

This program prints,

deferred call in fullName  
deferred call in main  
panic: runtime error: last name cannot be nil

goroutine 1 [running]:  
main.fullName(0x1042bf90, 0x0)  
    /tmp/sandbox060731990/main.go:13 +0x280
main.main()  
    /tmp/sandbox060731990/main.go:22 +0xc0      

當程式在第 13 行發生 panic 時,首先執行了延遲函數,接着控制傳回到函數調用方,調用方的延遲函數繼續運作,直到到達頂層調用函數。

在我們的例子中,首先執行 ​

​fullName​

​​ 函數中的 ​

​defer​

​ 語句(第 8 行)。程式列印出:

deferred call in fullName      

接着程式傳回到 ​

​main​

​​ 函數,執行了 ​

​main​

​ 函數的延遲調用,是以會輸出:

deferred call in main      

現在程式控制到達了頂層函數,是以該函數會列印出 panic 資訊,然後是堆棧跟蹤,最後終止程式。

recover

​recover​

​ 是一個内建函數,用于重新獲得 panic 協程的控制。

​recover​

​ 函數的标簽如下所示:

func recover() interface{}      

隻有在延遲函數的内部,調用 ​

​recover​

​​ 才有用。在延遲函數内調用 ​

​recover​

​​,可以取到 ​

​panic​

​​ 的錯誤資訊,并且停止 panic 續發事件(Panicking Sequence),程式運作恢複正常。如果在延遲函數的外部調用 ​

​recover​

​,就不能停止 panic 續發事件。

我們來修改一下程式,在發生 panic 之後,使用 ​

​recover​

​ 來恢複正常的運作。

package main

import (  
    "fmt"
)

func recoverName() {  
    if r := recover(); r!= nil {
        fmt.Println("recovered from ", r)
    }
}

func fullName(firstName *string, lastName *string) {  
    defer recoverName()
    if firstName == nil {
        panic("runtime error: first name cannot be nil")
    }
    if lastName == nil {
        panic("runtime error: last name cannot be nil")
    }
    fmt.Printf("%s %s\n", *firstName, *lastName)
    fmt.Println("returned normally from fullName")
}

func main() {  
    defer fmt.Println("deferred call in main")
    firstName := "Elon"
    fullName(&firstName, nil)
    fmt.Println("returned normally from main")
}      

​​在 playground 上運作​​

在第 7 行,​

​recoverName()​

​​ 函數調用了 ​

​recover()​

​​,傳回了調用 ​

​panic​

​​ 的傳參。在這裡,我們隻是列印出 ​

​recover​

​​ 的傳回值(第 8 行)。在 ​

​fullName​

​​ 函數内,我們在第 14 行延遲調用了 ​

​recoverNames()​

​。

當 ​

​fullName​

​​ 發生 panic 時,會調用延遲函數 ​

​recoverName()​

​​,它使用了 ​

​recover()​

​ 來停止 panic 續發事件。

該程式會輸出:

recovered from  runtime error: last name cannot be nil  
returned normally from main  
deferred call in main      

當程式在第 19 行發生 panic 時,會調用延遲函數 ​

​recoverName​

​​,它反過來會調用 ​

​recover()​

​​ 來重新獲得 panic 協程的控制。第 8 行調用了 ​

​recover​

​​,傳回了 ​

​panic​

​ 的傳參,是以會列印:

recovered from  runtime error: last name cannot be nil      

在執行完 ​

​recover()​

​​ 之後,panic 會停止,程式控制傳回到調用方(在這裡就是 ​

​main​

​​ 函數),程式在發生 panic 之後,從第 29 行開始會繼續正常地運作。程式會列印 ​

​returned normally from main​

​​,之後是 ​

​deferred call in main​

​。

panic,recover 和 Go 協程

隻有在相同的 ​​Go 協程​​​中調用 recover 才管用。​

​recover​

​ 不能恢複一個不同協程的 panic。我們用一個例子來了解這一點。

package main

import (  
    "fmt"
    "time"
)

func recovery() {  
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}

func a() {  
    defer recovery()
    fmt.Println("Inside A")
    go b()
    time.Sleep(1 * time.Second)
}

func b() {  
    fmt.Println("Inside B")
    panic("oh! B panicked")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}      

​​在 playground 上運作​​

在上面的程式中,函數 ​

​b()​

​​ 在第 23 行發生 panic。函數 ​

​a()​

​​ 調用了一個延遲函數 ​

​recovery()​

​​,用于恢複 panic。在第 17 行,函數 ​

​b()​

​​ 作為一個不同的協程來調用。下一行的 ​

​Sleep​

​​ 隻是保證 ​

​a()​

​​ 在 ​

​b()​

​ 運作結束之後才退出。

你認為程式會輸出什麼?panic 能夠恢複嗎?答案是否定的,panic 并不會恢複。因為調用 ​

​recovery​

​​ 的協程和 ​

​b()​

​ 中發生 panic 的協程并不相同,是以不可能恢複 panic。

運作該程式會輸出:

Inside A  
Inside B  
panic: oh! B panicked

goroutine 5 [running]:  
main.b()  
    /tmp/sandbox388039916/main.go:23 +0x80
created by main.a  
    /tmp/sandbox388039916/main.go:17 +0xc0      

從輸出可以看出,panic 沒有恢複。

如果函數 ​

​b()​

​ 在相同的協程裡調用,panic 就可以恢複。

如果程式的第 17 行由 ​

​go b()​

​​ 修改為 ​

​b()​

​,就可以恢複 panic 了,因為 panic 發生在與 recover 相同的協程裡。如果運作這個修改後的程式,會輸出:

Inside A  
Inside B  
recovered: oh! B panicked  
normally returned from main      

運作時 panic

運作時錯誤(如數組越界)也會導緻 panic。這等價于調用了内置函數 ​

​panic​

​​,其參數由接口類型 ​​runtime.Error​​​ 給出。​

​runtime.Error​

​ 接口的定義如下:

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

而 ​

​runtime.Error​

​​ 接口滿足内建接口類型 ​​error​​。

我們來編寫一個示例,建立一個運作時 panic。

package main

import (  
    "fmt"
)

func a() {  
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}
func main() {  
    a()
    fmt.Println("normally returned from main")
}      

​​在 playground 上運作​​

在上面的程式中,第 9 行我們試圖通路 ​

​n[3]​

​​,這是一個對​​切片​​的錯誤引用。該程式會發生 panic,輸出如下:

panic: runtime error: index out of range

goroutine 1 [running]:  
main.a()  
    /tmp/sandbox780439659/main.go:9 +0x40
main.main()  
    /tmp/sandbox780439659/main.go:13 +0x20      

你也許想知道,是否可以恢複一個運作時 panic?當然可以!我們來修改一下上面的代碼,恢複這個 panic。

package main

import (  
    "fmt"
)

func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
    }
}

func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}      

​​在 playground 上運作​​

運作上面程式會輸出:

Recovered runtime error: index out of range  
normally returned from main      

從輸出可以知道,我們已經恢複了這個 panic。

恢複後獲得堆棧跟蹤

當我們恢複 panic 時,我們就釋放了它的堆棧跟蹤。實際上,在上述程式裡,恢複 panic 之後,我們就失去了堆棧跟蹤。

有辦法可以列印出堆棧跟蹤,就是使用 ​​Debug​​​ 包中的 ​​PrintStack​​ 函數。

package main

import (  
    "fmt"
    "runtime/debug"
)

func r() {  
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
        debug.PrintStack()
    }
}

func a() {  
    defer r()
    n := []int{5, 7, 4}
    fmt.Println(n[3])
    fmt.Println("normally returned from a")
}

func main() {  
    a()
    fmt.Println("normally returned from main")
}      

​​在 playground 上運作​​

在上面的程式中,我們在第 11 行使用了 ​

​debug.PrintStack()​

​ 列印堆棧跟蹤。

該程式會輸出:

Recovered runtime error: index out of range  
goroutine 1 [running]:  
runtime/debug.Stack(0x1042beb8, 0x2, 0x2, 0x1c)  
    /usr/local/go/src/runtime/debug/stack.go:24 +0xc0
runtime/debug.PrintStack()  
    /usr/local/go/src/runtime/debug/stack.go:16 +0x20
main.r()  
    /tmp/sandbox949178097/main.go:11 +0xe0
panic(0xf0a80, 0x17cd50)  
    /usr/local/go/src/runtime/panic.go:491 +0x2c0
main.a()  
    /tmp/sandbox949178097/main.go:18 +0x80
main.main()  
    /tmp/sandbox949178097/main.go:23 +0x20
normally returned from main      
  • 什麼是 panic?
  • 什麼時候應該使用 panic?
  • panic 示例
  • 發生 panic 時的 defer
  • recover
  • panic,recover 和 Go 協程
  • 運作時 panic
  • 恢複後獲得堆棧跟蹤