天天看點

go語言學習-panic 和 recover (三十二)

翻譯自:https://golangbot.com/panic-and-recover/

什麼是 panic?

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

但在有些情況,當程式發生異常時,無法繼續運作。在這種情況下,我們會使用

panic

來終止程式。當函數發生

panic

時,它會終止運作,在執行完所有的

defer

函數後,程式控制傳回到該函數的調用方。這樣的過程會一直持續下去,直到目前協程的所有函數都傳回退出,然後程式會列印出

panic

資訊,接着列印出

堆棧跟蹤

(

Stack Trace

),最後程式終止。當我們編寫一個示例程式後,我們就能很好地了解這個概念了。

我們将在本教程後面讨論,當程式發生

panic

時,使用

recover

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

panic

recover

可以認為類似于其他語言中的

try-catch-finally

語句,雖然它使用起來更優雅,代碼更簡潔,隻不過我們很少使用它。

什麼時候應該使用 panic?

需要注意的是,你應該盡可能地使用錯誤處理,而不是使用

panic

recover

。隻有當程式不能繼續運作的時候,才應該使用

panic

recover

機制。

panic

有兩個合理的用例:

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

内置函數

panic

的簽名如下所示:

當程式終止時,會列印傳入

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")
}
           

上面的程式很簡單,會列印一個人的全名。第

7

行的

fullName

函數會列印出一個人的全名。該函數在第

8

行和第

11

行分别檢查了

firstName

lastName

的指針是否為

nil

。如果是

nil

fullName

函數會調用含有不同的錯誤資訊的

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

是如何工作的,并且當程式發生

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

行,因為

fullName

函數調用導緻發生了

panic

,是以接下來會列印出:

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

現在我們已經到達了導緻

panic

的頂層函數,這裡沒有更多的層級,是以結束列印。

發生 panic 時的 defer

讓我們回想一下

panic

的作用。當函數遇到

panic

時,将停止執行,執行所有

defer

函數,然後程式流程傳回到調用方。此過程一直持續到目前協程的所有函數都傳回,此時程式列印出

panic

消息,然後是堆棧跟蹤,最終程式終止。

在上面的示例中,我們沒有延遲調用任何函數。如果存在延遲函數調用,則執行該調用,然後程式流程傳回到調用方。

讓我們稍微修改上面的例子并使用

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")
}
           

上述代碼中,我們隻修改了兩處,分别在第

8

行和第

20

行添加了延遲函數的調用。

運作程式将會列印:

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

函數的簽名如下所示:

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

recover

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

recover

,可以取到

panic

的錯誤資訊,并且阻止

panic

後續事件發生,程式運作恢複正常。如果在延遲函數的外部調用

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")
}
           

在第

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

的傳參,是以會列印:

在執行完

recover()

之後,

panic

會停止,程式控制傳回到調用方(

在這裡就是 main 函數

),程式在發生

panic

之後,從第

29

行開始會繼續正常地運作。程式會列印

returned normally from main

,之後是

deferred call in main

panic,recover 和 協程

隻有在同一個協程中調用

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")
}
           

在上面的程式中,函數

b()

在第

23

行發生

panic

。函數

a()

調用了一個延遲函數

recovery()

,用于恢複

panic

。在第

17

行,建立了一個新的協程。下一行的

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")
}
           

在上面的程式第

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")
}
           

運作上面的程式将輸出,

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")
}
           

在上面的程式中,我們在第

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

本教程到此結束。

簡單概括一下本教程讨論的内容:

  • 什麼是panic?
  • 何時應該使用panic?
  • panic的示例
  • 發生 panic 時的 defer
  • recover
  • panic,recover 和 協程
  • 運作時 panic
  • 恢複後擷取堆棧跟蹤

繼續閱讀