golang教程之Panic and Recover

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/wyy626562203/article/details/83411640

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返回。

猜你喜欢

转载自blog.csdn.net/wyy626562203/article/details/83411640