1. 程式人生 > >golang教程之Panic and Recover

golang教程之Panic and Recover

文章目錄

Panic and Recover

原文:https://golangbot.com/panic-and-recover/

在這裡插入圖片描述

什麼是panic?

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

但是在某些情況下程式不能簡單地在異常情況下繼續執行。在這種情況下,我們使用panic來終止程式。當函式遇到panic時,將停止執行,執行任何延遲函式,然後控制權返回其呼叫者。此過程一直持續到當前goroutine的所有函式都返回,此時程式打印出緊急訊息,然後是堆疊跟蹤,然後終止。當我們編寫示例程式時,這個概念會更加清晰。

可以使用recover 重新控制panic程式,我們將在本教程後面討論。

panic和recover可以被認為類似於其他語言中的try-catch-finally語句。

何時應該使用panic

一個重要因素是你應該避免panicrecover並使用錯誤。只有在程式無法繼續執行的情況下才應使用恐慌和機制。

panic有兩個有效的用例。

  • 一個不可恢復的錯誤,程式不能簡單地繼續執行它。

    一個例子是無法繫結到所需埠的Web伺服器。在這種情況下,panic是合理的,因為如果埠繫結本身失敗則沒有別的辦法。

  • 程式設計師錯誤。
    假設我們有一個接受指標作為引數的方法,有人使用nil作為引數呼叫此方法。在這種情況下,我們可能會感到恐慌,因為呼叫帶有nil引數的方法的程式設計師錯誤是期望有效的指標。

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

以上是列印一個人全名的簡單程式。 第7行中的fullName函式列印一個人的全名。 此函式檢查firstNamelastName指標是否為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。 因此,遇到panic時,程式執行終止,列印傳遞給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

讓我們回想一下panic的作用。 當函式遇到panic時,將停止執行,執行延遲函式,然後控制權返回其呼叫者。 此過程一直持續到當前goroutine的所有函式都返回,此時程式打印出緊急訊息,然後是堆疊跟蹤,然後終止。

在上面的示例中,我們沒有推遲任何函式呼叫。 如果存在延遲函式呼叫,則執行該呼叫,然後控制元件返回其呼叫者。

讓我們稍微修改上面的例子並使用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

當程式panic時,首先執行延遲函式呼叫,然後控制權返回到執行延遲呼叫的呼叫者,依此類推,直到達到頂級呼叫者。

在我們的案例中,延遲宣告在第8行中。 首先執行fullName函式。 這列印

deferred call in fullName  

然後控制權返回到執行延遲呼叫的main函式,因此打印出來,

deferred call in main  

現在控制權已達到頂級功能,因此程式列印緊急訊息,然後是堆疊跟蹤,然後終止。

Recover

recover是一個內建函式,用於重新控制panic goroutine。

recover函式的簽名如下,

func recover() interface{}  

只有在延遲函式內部呼叫時,recover才有用。 執行呼叫以在延遲函式內恢復可通過恢復正常執行來停止panic序列,並檢索傳遞給恐慌呼叫的錯誤值。 如果在延遲函式之外呼叫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")
}

第7行中的recoverName()函式呼叫recover()返回傳遞給panic呼叫的值。 這裡我們只是列印recover行返回的值。在第14行中延遲recoverName()fullName函式內。

fullName發生混亂時,將呼叫延遲函式recoverName(),它使用recover()來停止panic序列。

這個程式將列印,

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

當程式panic時,將呼叫延遲的recoverName函式,然後呼叫recover()來重新控制恐慌goroutine。 在第8行中呼叫recover()。 從panic中返回引數,因此打印出來,

recovered from  runtime error: last name cannot be nil  

在執行recover()之後,panic停止並且控制返回到呼叫者,在這種情況下,主函式和程式在panic之後繼續從主右邊的第29行正常執行。 它列印通常從main返回,然後在main中延遲呼叫

panic,recover和Goroutines

Recover 僅在從同一goroutine呼叫時才起作用。 從不同的goroutine發生的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中恢復。 函式b()是單獨的goroutine。並且下一行中的Sleep只是為了確保程式在b()執行完畢之前不會終止。

您認為該計劃的產出是什麼? panic會被恢復嗎? 答案是不。 panic將無法恢復。 這是因為recovery函式存在於不同的gouroutine中,並且panic發生在函式b()中的不同goroutine中。 因此無法恢復。

執行此程式將輸出,

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就會被恢復。

 go b()

修改為

b()  

由於panic發生在同一個goroutine,現在將恢復。 如果程式執行上面的更改,它將輸出,

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

執行時Panics

Panics也可能由執行時錯誤引起,例如陣列越界訪問。 這相當於使用由介面型別runtime.Error定義的引數呼叫內建函式panicruntime.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介面滿足內建介面型別錯誤。

讓我們寫一個創造執行時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恢復後通常從main返回。