[譯] Part 32: 詳解Golang 中的Panic 和 Recover

咔嘰咔嘰發表於2019-03-30

什麼是panic?

處理Go中異常情況的慣用方法是使用errors,對於程式中出現的大多數異常情況,errors就足夠了。

但是在某些情況下程式不能在異常情況下繼續正常執行。在這種情況下,我們使用panic來終止程式。函式遇到panic時將會停止執行,如果有defer的話就執行defer延遲函式,然後返回其呼叫者。此過程一直持續到當前goroutine的所有函式都返回,然後列印出panic資訊,然後是堆疊資訊,然後程式終止。待會兒用一個例子來解釋,這個概念就會更加清晰一些了。

我們可以使用recover函式恢復被panic終止的程式,將在本教程後面討論。

panic和recover有點類似於其他語言中的try-catch-finally語句,但是前者使用的比較少,而且使用時更優雅程式碼也更簡潔。

什麼時候應該用panic?

一般情況下我們應該避免使用panic和recover,儘可能使用errors。只有在程式無法繼續執行的情況下才應該使用panic和recover。

兩個panic典型應用場景
  1. 不可恢復的錯誤,讓程式不能繼續進行。 比如說Web伺服器無法繫結到指定埠。在這種情況下,panic是合理的,因為如果埠繫結失敗接下來的邏輯繼續也是沒有意義的。

  2. coder的人為錯誤 假設我們有一個接受指標作為引數的方法,然而使用了nil作為引數呼叫此方法。在這種情況下,我們可以用panic,因為該方法需要一個有效的指標。

panic示例

panic函式的定義

func panic(interface{})
複製程式碼

當程式終止時,引數會傳遞給panic函式列印出來。看看下面例子的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")
}
複製程式碼

Run in playground

上面這段程式碼,fullName函式功能是列印一個人的全名。此函式檢查firstName和lastName指標是否為nil。如果它為nil,則函式呼叫panic並顯示相應的錯誤訊息。程式終止時將列印此錯誤訊息和錯誤堆疊資訊。

執行此程式將列印以下輸出,

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是如何工作以及如何列印堆疊跟蹤的。 在第19行,我們將Elon定義給firstName。然後呼叫fullName函式,其中lastName引數為nil。因此,第11行將觸發panic。當觸發panic時,程式執行就終止了,然後列印傳遞給panic的內容,最後列印堆疊跟蹤資訊。因此14行以後的程式碼不會被執行。 該程式首先列印傳遞給panic函式的內容,

panic: runtime error: last name cannot be nil
複製程式碼

然後列印堆疊跟蹤資訊。 該程式在12行觸發panic,因此,

ain.fullName(0x1040c128, 0x0)
    /tmp/sandbox135038844/main.go:12 +0x120
複製程式碼

將被首先列印。然後將列印堆疊中的下一個內容,

main.main()
    /tmp/sandbox135038844/main.go:20 +0x80
複製程式碼

現在已經返回到了造成panic的頂層main函式,因此列印結束。

defer函式

我們回想一下panic的作用。當函式遇到panic時,將會終止panic後面程式碼的執行,如果函式體包含有defer函式的話會執行完defer函式。然後返回其呼叫者。此過程一直持續到當前goroutine的所有函式都返回,此時程式列印出panic內容,然後是堆疊跟蹤資訊,然後終止。

在上面的示例中,我們沒有任何defer函式的呼叫。修改下上面的例子,來看看defer函式的例子吧。

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")
}
複製程式碼

Run in playground 對之前程式碼所做的唯一更改是在fullName函式和main函式中第一行新增了defer函式呼叫。 執行的輸出,

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
複製程式碼

當發生panic時,首先執行defer函式,然後到下一個defer呼叫,依此類推,直到達到頂層呼叫者。

在我們的例子中,defer宣告在fullName函式的第一行。首先執行fullName函式。列印

deferred call in fullName
複製程式碼

然後呼叫返回到main函式的defer,

deferred call in main
複製程式碼

現在呼叫已返回到頂層函式,然後程式列印panic內容,然後是堆疊跟蹤資訊,然後終止。

recover函式

recover是一個內建函式,用於goroutine從panic的中斷狀況中恢復。 函式定義如下,

func recover() interface{}
複製程式碼

recover只有在defer函式內部呼叫時才有效。defer函式內通過呼叫recover可以讓panic中斷的程式恢復正常執行,呼叫recover會返回panic的內容。如果在defer函式之外呼叫recover,它將不會停止panic序列。

修改一下,使用recover來讓panic恢復正常執行。

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")
}
複製程式碼

Run in playground 第7行呼叫了recoverName函式。這裡列印了recover返回的值, 發現recover返回的是panic的內容。

列印如下,

recovered from  runtime error: last name cannot be nil
returned normally from main
deferred call in main
複製程式碼

程式在19行觸發panic,defer函式recoverName通過呼叫recover來重新控制該goroutine,

recovered from  runtime error: last name cannot be nil
複製程式碼

在執行recover之後,panic停止並且返回到呼叫者,main函式和程式在觸發panic之後將繼續從第29行執行。然後列印,

returned normally from main
deferred call in main
複製程式碼

Panic, Recover 和 Goroutines

recover僅在從同一個goroutine呼叫時才起作用。從不同的goroutine觸發的panic中recover是不可能的。再來一個例子來加深理解。

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")
}
複製程式碼

Run in playground 在上面的程式中,函式b在23行觸發panic。函式a呼叫defer函式recovery用於從panic中恢復。函式a的17行用另外一個goroutine執行b函式。Sleep的作用只是為了確保程式在b執行完畢之前不會被終止,當然也可以用sync.WaitGroup來解決。

你認為該段程式碼的輸出是什麼?panic會被恢復嗎?答案是不可以。panic將無法被恢復。這是因為recover存在於不同的gouroutine中,並且觸發panic發生在不同goroutine執行的b函式。因此無法恢復。 執行的輸出,

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
複製程式碼

可以從輸出中看到恢復失敗了。

如果在同一個goroutine中呼叫函式b,那麼panic就會被恢復。 在第17行把, go b() 換成 b() 那麼會輸出,

Inside A
Inside B
recovered: oh! B panicked
normally returned from main
複製程式碼

執行時的panic

panic還可能由執行時的錯誤引起,例如陣列越界訪問。這相當於使用由介面型別runtime.Error定義的引數呼叫內建函式panic。 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")
}
複製程式碼

Run in 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")
}
複製程式碼

Run in playground 執行後輸出,

Recovered runtime error: index out of range
normally returned from main
複製程式碼

顯然可以看到panic被恢復了。

recover後獲取堆疊資訊

我們恢復了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")
}
複製程式碼

Run in 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被恢復然後列印Recovered runtime error: index out of range,再然後列印堆疊跟蹤資訊。最後在panic被恢復後列印normally returned from main

相關文章