美文网首页
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教程第三十篇:故障及恢复

    panic-and-recover 本文是《Go系列教程》的第三十篇文章。 什么是panic ? Go程序处理异常...

  • 01 Go极简教程 目录

    极简教程的初衷是给已有其他语言基础的人阅读尽可能少的内容学习Go语言 Go极简教程 目录 Go极简教程 环境安装及...

  • Day010-MySQL备份恢复与迁移

    1. 企业的备份恢复案例(mysqldump+binlog),年终故障恢复演练。(项目案例) 案例模拟及恢复: 1...

  • 故障恢复

    Rabbit的负载均衡 使用HAProxy负载均衡 image.png1.server:后台服务器定义...

  • Go教程第三篇:变量

    Go教程第三篇:变量 本文是《Golang系列教程》第三篇文章,它主要讲解golang中变量的用法。 什么是变量 ...

  • etcd学习笔记(四):恢复

    运维一个etcd集群的一个基本要求是能够故障恢复。etcd有哪些机制支持故障恢复?如何进行故障恢复? 持久化 ex...

  • go语言学习资料

    菜鸟教程-Go 语言教程 https://www.runoob.com/go/go-tutorial.html老男...

  • ZABBIX 监控告警模板

    比较简洁漂亮的模板 故障告警模板 故障恢复模板

  • 主从复制(高级阶段)

    01,延时从库 1.介绍 2.使用延时从库的原因 3.配置延时从库 4.延时从库应用 ①故障恢复思路 ②故障模拟及...

  • redis持久化

    为了更好应对故障恢复。

网友评论

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

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