文章目录
Panic and Recover
什么是panic?
处理Go中程序异常情况的惯用方法是使用错误。对于程序中出现的大多数异常情况,错误就足够了。
但是在某些情况下程序不能简单地在异常情况下继续执行。在这种情况下,我们使用panic
来终止程序。当函数遇到panic
时,将停止执行,执行任何延迟函数,然后控制权返回其调用者。此过程一直持续到当前goroutine的所有函数都返回,此时程序打印出紧急消息,然后是堆栈跟踪,然后终止。当我们编写示例程序时,这个概念会更加清晰。
可以使用recover
重新控制panic
程序,我们将在本教程后面讨论。
panic和recover可以被认为类似于其他语言中的try-catch-finally语句。
何时应该使用panic
?
一个重要因素是你应该避免panic
和recover
并使用错误。只有在程序无法继续执行的情况下才应使用恐慌和机制。
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
函数打印一个人的全名。 此函数检查firstName
和lastName
指针是否为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
定义的参数调用内置函数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
接口满足内置接口类型错误。
让我们写一个创造运行时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返回。