Go教程第三十篇:故障及恢复

2020-07-13  本文已影响0人  大风过岗

panic-and-recover

本文是《Go系列教程》的第三十篇文章。

什么是panic ?

Go程序处理异常条件的惯用方式是使用error。对于在程序中引起的大多数异常条件,error都能够满足要求。但是有些情况下,当程序发生异常情况的时候,程序会无法继续运行。在这种情况下,我们可以使用panic来提前终止程序。当函数发生panic之后,它的执行就会停止。任何延迟执行的函数都会被执行,之后程序的控制逻辑返回到它的调用者。此过程会一直持续到当前goroutine的所有函数都已返回。我们下面会写一个示例程序,帮助大家理解。

在后面我们会讨论,使用recover也能重新获得对故障程序的控制。

Go中的故障恢复和其他语言中的try-catch-finally的语义类似。像java异常这样的,在Go中几乎很少使用。

什么时候使用panic?

有一个重要的要素是,你应该避免panic和recover,并尽可能地使用error来处理异常情况。只有当程序无法继续运行的时候,我们才能使用panic-recover机制。

这里有俩个有效的使用场景:

1、发生了无法自动恢复的错误,此时程序不能继续往下运行。
举个例子,一个web服务器,无法绑定到所需的端口,在这种情况下,就应该使用panic。因为如果端口绑定失败的话,程序就不能执行任何操作。

2、编程错误

我们说,我们有一个方法接收一个指针类型的参数,而某个人调用此方法时,传递了一个nil参数。在这种情况下,我们就可以使用panic,因为这是一个编程错误,方法要求的是一个有效的指针值参数,而传递的确是一个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")
}

上面这个是一个简单的程序,它会把人的姓名打印出来。fullName函数负责打印人员的姓名。这个函数会分别检查firstName和lastName是否为nil。如果为nil的话,就会调用panic,并传递一个对应的消息。当程序结束的时候,会把此消息打印出来。
运行此程序,会得到如下打印:

panic: runtime error: last name cannot be nil

goroutine 1 [running]:
main.fullName(0xc00006af58, 0x0)
    /tmp/sandbox210590465/prog.go:12 +0x193
main.main()
    /tmp/sandbox210590465/prog.go:20 +0x4d

我们来分析一下这个输出,以便于理解panic是如何工作的,以及当程序panic的时候,如何打印的错误堆栈信息。
在19行,我们把Elon赋值给了firstName。之后,我们调用了fullName()函数,传递的参数分别为:“Elon”和nil。
因此,程序将发生panic。当程序发生panic时,程序会结束运行,在打印堆栈之后,会紧接着把传递给panic的参数打印出来。由于程序在panic函数调用之后,就结束运行了,因此后面的代码也就得不到运行的机会。

程序会首先打印传递给panic函数的消息。

panic: runtime error: last name cannot be nil

之后,打印堆栈信息。
在fullName的第12行,程序发生panic,故而先打印:

goroutine 1 [running]:
main.fullName(0xc00006af58, 0x0)
    /tmp/sandbox210590465/prog.go:12 +0x193

之后,打印堆栈中的下一个异常信息,因此打印输出为:

main.main()
    /tmp/sandbox210590465/prog.go:20 +0x4d

到此为止,我们就到达了导致此panic的顶层函数,其上层在没有其他信息,因此,也就没有别的输出了。

panic示例二

在运行期间发生的error也会导致panic,例如,试图访问一个数组中不存在的角标等。

我们来写一个人为的示例,展示一下越界访问也会导致panic。

package main

import (
    "fmt"
)

func slicePanic() {
    n := []int{5, 7, 4}
    fmt.Println(n[4])
    fmt.Println("normally returned from a")
}
func main() {
    slicePanic()
    fmt.Println("normally returned from main")
}

在上面的程序中,我们试图访问n[4],而角标4却压根在数组中不存在。程序此时会发生panic,并
产生如下输出:

panic: runtime error: index out of range [4] with length 3

goroutine 1 [running]:
main.slicePanic()
    /tmp/sandbox942516049/prog.go:9 +0x1d
main.main()
    /tmp/sandbox942516049/prog.go:13 +0x22

在panic期间进行Defer调用

我们来整理一下panic都会做什么工作。当一个函数发生panic时,它的执行会立即停止,任何延迟函数都会被执行,之后控制逻辑返回到他们的调用者。此过程会一直持续直到当前Goroutine的所有函数都从打印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(0xc00006af28, 0x0)
    /tmp/sandbox451943841/prog.go:13 +0x23f
main.main()
    /tmp/sandbox451943841/prog.go:22 +0xc6

当程序在13行发生panic的时候,任何的延迟函数调用都会被首先执行,之后控制逻辑返回到延迟函数的调用者手中。
在我们的例子中,首先执行fullName的defer语句,它会打印下面这些信息。

deferred call in fullName

之后,控制逻辑会返回到main函数手中,故而此时会执行main函数中的延迟调用,因此打印输出如下:

deferred call in main

现在控制逻辑已经到达了顶层函数,因此程序会打印panic消息,紧接着的是异常堆栈,之后,程序会结束运行。

从panic中恢复

recover是一个内置函数,它可用于重获对panic程序的控制。

recover函数的签名如下:

func recover() interface{}

recover只有在延迟函数内部调用的时候才有用。在延迟函数内部执行recover调用, 会停止后续的panic,恢复正常的调用,并获取传递给panic函数的错误信息。如果recover在延迟函数外部调用的话,它不会停止后续的panic。

我们对上面的程序进行修改,并在发生panic之后,使用recover来恢复正常运行。

package main

import (
    "fmt"
)

func recoverFullName() {
    if r := recover(); r!= nil {
        fmt.Println("recovered from ", r)
    }
}

func fullName(firstName *string, lastName *string) {
    defer recoverFullName()
    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")
}

第七行的recoverFullName()函数调用了recover函数,recover函数会返回传递给panic函数的返回值。因此,我们仅打印出了recover函数返回的值。recoverFullName()又在fullName函数的第14行被延迟调用。

当fullName函数发生panic的时候,延迟函数recoverName()将会被调用,而recoverName()却又会使用recover来停止后续的panic。
程序将输出如下:

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

当程序在第19行发生panic时,延迟函数recoverFullName会被调用,而recoverFullName又会调用recover(),recover会重获控制。
recover()调用会返回传递给panic函数的参数,因此它会打印:

recovered from  runtime error: last name cannot be nil

在recover执行完之后,就会停止panic,控制逻辑返回到main函数手中。从29行之后,程序会继续正常执行,因为panic已经被恢复了。它打印了“returned normally from main”,
紧接着又打印出了"deferred call in main"。

我们再来看一个例子,在这个例子中我们会使用recover从由于访问数组无效角标的导致的panic中恢复。

package main

import (
    "fmt"
)

func recoverInvalidAccess() {
    if r := recover(); r != nil {
        fmt.Println("Recovered", r)
    }
}

func invalidSliceAccess() {
    defer recoverInvalidAccess()
    n := []int{5, 7, 4}
    fmt.Println(n[4])
    fmt.Println("normally returned from a")
}

func main() {
    invalidSliceAccess()
    fmt.Println("normally returned from main")
}

运行上面的程序,会得到如下输出:

Recovered runtime error: index out of range [4] with length 3
normally returned from main

从上面的输出中,你就会看到,我们已经从panic中恢复啦。

故障恢复之后,获取堆栈信息

如果我们从panic中恢复,我们就丢失了此panic的异常堆栈。甚至在上面的程序恢复之后,我们丢失了堆栈。有一种方式可以打印堆栈信息,那就是使用Debug包下的PrintStack函数。

package main

import (
    "fmt"
    "runtime/debug"
)

func recoverFullName() {
    if r := recover(); r != nil {
        fmt.Println("recovered from ", r)
        debug.PrintStack()
    }
}

func fullName(firstName *string, lastName *string) {
    defer recoverFullName()
    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")
}

在上面的程序中,我们使用debug.PrintStack()来打印堆栈信息。程序的输出如下:

recovered from  runtime error: last name cannot be nil
goroutine 1 [running]:
runtime/debug.Stack(0x37, 0x0, 0x0)
    /usr/local/go-faketime/src/runtime/debug/stack.go:24 +0x9d
runtime/debug.PrintStack()
    /usr/local/go-faketime/src/runtime/debug/stack.go:16 +0x22
main.recoverFullName()
    /tmp/sandbox771195810/prog.go:11 +0xb4
panic(0x4a1b60, 0x4dc300)
    /usr/local/go-faketime/src/runtime/panic.go:969 +0x166
main.fullName(0xc0000a2f28, 0x0)
    /tmp/sandbox771195810/prog.go:21 +0x1cb
main.main()
    /tmp/sandbox771195810/prog.go:30 +0xc6
returned normally from main
deferred call in main

从输出中,我们可以理解,panic已经被恢复了,并且打印了"recovered from runtime error: last name cannot be nil"。
之后,就是打印的堆栈信息,之后在panic恢复之后,打印出了:

returned normally from main
deferred call in main

Panic,Recover 以及Goroutine

只有当recover和panic在同一个goroutine中时,recover才可以正常工作。如果发生panic的goroutine和recover()函数所在的goroutine不是同一个的话,也就无法恢复。我们写段示例来理解一下。

package main

import (
    "fmt"
)

func recovery() {
    if r := recover(); r != nil {
        fmt.Println("recovered:", r)
    }
}

func sum(a int, b int) {
    defer recovery()
    fmt.Printf("%d + %d = %d\n", a, b, a+b)
    done := make(chan bool)
    go divide(a, b, done)
    <-done
}

func divide(a int, b int, done chan bool) {
    fmt.Printf("%d / %d = %d", a, b, a/b)
    done <- true

}

func main() {
    sum(5, 0)
    fmt.Println("normally returned from main")
}

在上面的程序中,函数divide()会发生panic,因为b是零,它无法被任何数除。sum()函数调用了一个延迟函数recovery(),该函数专门用于从panic中恢复。函数divide()作为一个单独的goroutine被调用。我们在done通道上等待,以确保divide执行完成。

你可以想象一下,程序的输出是什么。panic可以被恢复吗? 答案是否。panic无法被恢复。这是因为recovery函数位于不同的goroutine中。而panic发生在另一个goroutine的divide()函数中,因此,无法从panic恢复。
运行此程序将得到如下输出:

5 + 0 = 5
panic: runtime error: integer divide by zero

goroutine 18 [running]:
main.divide(0x5, 0x0, 0xc0000a2000)
    /tmp/sandbox877118715/prog.go:22 +0x167
created by main.sum
    /tmp/sandbox877118715/prog.go:17 +0x1a9

从输出中,你可以看到恢复并没有发生。如果divide()函数是在同一个goroutine中被调用的话,我们就可以从panic中恢复过来。
在程序的17行,我们把程序由:

go divide(a, b, done)

修改为:

divide(a, b, done)

此时,恢复就可以正常运行,因为panic是在同一个goroutine中发生的。把程序做如上修改,运行之后,它将输出如下:

5 + 0 = 5
recovered: runtime error: integer divide by zero
normally returned from main

备注
本文系翻译之作原文博客地址

上一篇下一篇

猜你喜欢

热点阅读