美文网首页
Go教程第三十篇:故障及恢复

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

作者: 大风过岗 | 来源:发表于2020-07-13 15:26 被阅读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
    
    

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

    相关文章

      网友评论

          本文标题:Go教程第三十篇:故障及恢复

          本文链接:https://www.haomeiwen.com/subject/mugacktx.html