Go36-21,22-panic函数recover函数以及defer语句

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go36-21,22-panic函数recover函数以及defer语句相关的知识,希望对你有一定的参考价值。

panic

panic,Go语言的另外一种错误处理方式。严格来讲,它处理的不是错误,而是异常,并且是一种在我们意料之外的程序异常。

panic详情

要分析panic详情,首先来生成一个panic。比如在一个切片里,它的长度是5,但是要通过索引5访问其中的元素,这样的访问是不正确的。比如下面这样:

func main() {
    l := []int{1, 2, 3, 4, 5}
    _ = l[5]
}

程序在运行时,会在执行到这行代码的时候抛出panic,提示用户索引越界了。这不仅仅是个提示。当panic被抛出后,如果没有在程序里添加任何保护措施的话,程序(或者说代表它的那个进程)会在打印出pinic的详细情况之后终止运行。下面是panic的详情:

panic: runtime error: index out of range

goroutine 1 [running]:
main.main()
        H:/Go/src/Go36/article21/example01/main.go:5 +0x3d
exit status 2

先看第一行的内容,其中的“runtime error”表示,这是一个runtime代码包中抛出的panic。在这个panic中,包含一个runtime.Error接口类型的值。runtime.Error接口内嵌了error接口并做了一点扩展,runtime包中有不少它的实现类型。实际上,在"panic:"右边的内容,就是这个panic包含的runtime.Error类型值的字符串表示形式。
此外,panic详情中一本还会包含与引发原因有关的goroutine的代码执行信息。这里的“goroutine 1 [running]:”表示是一个ID为1的goroutine在这个panic被引发的时候正在运行。
再看下一行,“main.main()”表明了这个goroutine包装的go函数就是命令源码文件里的main函数。再往下一行,指出了这个源码文件的绝对路径,以及代码在文件中所处的行。这一行的最后的+0x3d代表的是:此行代码相对于其所属函数的入口程序计数偏移量。不过,一般这个对我们没什么用。
最后的“exit status 2”,是这个程序退出的状态码。在大多数操作系统中,只要退出状态码不是0,就是非正常结束。在Go语言中,因panic导致的程序结束运行的退出状态码一般都会是2。

从panic被引发到程序终止运行的过程

先说一个大致的过程:当引发了一个panic。
这时,初始的panic详情会被建立起来,并且该程序的控制权会立即从此代码行转移至调用起所属函数的那行代码上,也就是调用栈中的上一级。这样就意味着,此行代码所属的函数的执行立即终止。
紧接着,控制权并不会停在当前位置,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的方向转播至顶端,就是我们编写的最外层的函数那里。这个最外层的函数就是go函数,对于主goroutine来说就是main函数。但是控制权到这还不会停止转移,而是被Go言语运行时的系统回收。
随后,程序崩溃并终止运行,运行这次程序的进程也会随之死亡并消失。而在这个控制权传播的过程中,panic详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。
这里再补充一下,函数引发panic与函数返回错误值的意义是完全不同的
当函数返回一个非nil的错误值时,函数的调用方有权选择不处理,并且不处理的后果往往是不致命的。
当一个panic发生时,如果不施加任何保护措施,那么导致的后果就是程序崩溃,这显然是致命的。
下面的例子清楚地展示了上面描述的控制权一级一级向上传播的过程:

package main

import "fmt"

func main() {
    fmt.Println("main Start")
    caller1()
    fmt.Println("main End")
}

func caller1() {
    fmt.Println("caller1 Start")
    caller2()
    fmt.Println("caller1 End")
}

func caller2() {
    fmt.Println("caller2 Start")
    l := []int{1, 2, 3, 4, 5}
    _ = l[5]
    fmt.Println("caller2 End")
}

这里,panic详情会在控制权传播的过程中,被逐渐地积累和完善。并且,控制权会一级一级地沿着调用栈的反方向传播至顶端。因此,针对某个goroutine的代码执行信息中,调用栈底端的信息会先出现,然后是上一级调用的信息。以此类推,最后才是此调用栈顶端的信息。
所以是,main函数调用了caller1函数,而caller1函数又调用了caller2函数。那么caller2函数中代码执行的信息会先出现,然后是caller1函数中代码的执行的信息,最后才是main函数的信息:

PS H:GosrcGo36article21example02> go run main.go
main Start
caller1 Start
caller2 Start
panic: runtime error: index out of range

goroutine 1 [running]:
main.caller2()
        H:/Go/src/Go36/article21/example02/main.go:20 +0xa2
main.caller1()
        H:/Go/src/Go36/article21/example02/main.go:13 +0x77
main.main()
        H:/Go/src/Go36/article21/example02/main.go:7 +0x77
exit status 2
PS H:GosrcGo36article21example02>

到这里,应该已经对panic被引发后的程序终止的过程有一定的了解了。深入了解这个过程以及正确的解读panic详情是一项必备技能。这在调试Go程序或为Go程序排查错误的时候非常有用。

panic函数、recover函数以及defer语句

如果一个panic是我们在无意间引发的,那么其中的值只能由Go语言运行时系统给定。但是,当我们使用panic函数有意的引发一个panic的时候,就可以自行指定其包含的值。

panic函数

在调用一个panic函数时,把某个值作为参数传递给函数就是可以了。panic函数只有一个参数,并且类型是空接口,所以从语法上讲,它可以接受任何类型的值。一旦程序异常了,就一定会把异常的相关信息记录下来,所以就会需用输出这个参数的字符串表示形式。虽然fmt.Sprintf和fmt.Fprintf这类可以格式化并输出参数的函数也符合要求。不过,在功能上推荐使用自定义的Error方法或者String方法。因此,为部不同的数据类型分别编写这两种方法是首选。这样,在程序崩溃的时候,panic包含的拿着值的字符串表示形式就会被打印出来:

package main

import (
    "fmt"
    // "errors"
)

func caller() {
    fmt.Println("caller Start")
    // panic(errors.New("Something Wrong"))  // 正例
    panic(fmt.Errorf("Something Wrong %s", "2"))  // 正例
    // panic(fmt.Println)  // 反例
}

func main() {
    fmt.Println("main Start")
    caller()
    fmt.Println("main End")
}

recover函数

可以施加应对panic的保护措施,避免程序崩溃。Go语言的内建函数recover专门用于恢复panic。recover无需任何参数,并且会返回一个空接口类型的值。这个返回值,就是panic传入的参数的副本。
下面先看一个错误的用法:

func main() {
    fmt.Println("main Start")
    panic(errors.New("Something Wrong"))
    p := reover()
    fmt.Println(p)
    fmt.Pringln("main End")
}

这里,引发panic之后,想紧接着调用recover函数。但是,函数的执行会在panic这行就终止了,这个recover函数的调用根本就没有机会执行。要想正确的调用recover函数,需要用到defer语句。下面是修正过的代码:

package main

import (
    "fmt"
    "errors"
)

func main() {
    fmt.Println("main Start")
    defer func() {
        fmt.Println("defer Start")
        if p := recover(); p != nil {
            fmt.Printf("Panic: %s
", p)
        }
        fmt.Println("defer End")
    }()
    panic(errors.New("Something Wrong"))
    fmt.Println("main End")
}

在这个main函数中,先编写了一条defer语句,并且再defer函数中调用了recover函数。仅当调用的结果不为nil时,也就是panic确实已经发生时,才会打印panic的内容。这里要尽量把defer语句写在函数体的开始处,因为引发panic语句之后的所有语句,都不会有任何执行的机会。

defer语句

defer语句就是被用来延迟执行代码的。延迟到该语句所在的函数即将执行结束的那一刻,无论结束执行的原因是什么(包括panic)。
与别的go语句类型,一个defer语句有一个defer关键字和一个调用表达式组成。这里存在一些限制,有一些调用表达式是不能出现在这里的:针对Go语句内奸函数的调用表达式,以及针对unsafe包中的函数的调用表达式。在这里被调用函数可以是有名称的,也可以是内名的。可以这这里的函数叫做defer函数或者延迟函数。注意,被延迟执行的是defer函数,而不是defer语句
defer执行顺序
如果一个函数中有多条defer语句的情况,那么defer函数的调用的执行顺序与它们所属的defer语句的执行顺序完全相反。当一个函数即将结束执行时,其中写在最下边的defer函数调用会最新执行,最上表的defer函数调用会最后一个执行。
在defer语句每次执行的时候,Go语言会把它所携带的defer函数及其参数值另行存储到一个队列中。这个队列与该defer语句所属的函数是对应的,并且它是先进后出(FILO)的,想到与一个栈。在需要执行某个函数的defer函数调用的时候,Go语言会先拿到对应的队列,然后从该队列中一个一个的取出defer函数及其参数值并逐个执行调用。这就是实现这个执行顺序的原因了。
下面是一个简单的示例,展示了defer的调用顺序:

package main

import "fmt"

func main() {
    defer fmt.Println("first defer")
    for i := 0; i < 3; i++ {
        defer fmt.Printf("defer in for [%d]
", i)
    }
    defer fmt.Println("last defer")
}

以上是关于Go36-21,22-panic函数recover函数以及defer语句的主要内容,如果未能解决你的问题,请参考以下文章

go defer,panic,recover详解 go 的异常处理

3. Go中panic与recover注意事项

GO语言异常处理机制panic和recover分析

Go基础系列:deferpanic和recover

go 错误处理panic recover

go语言学习笔记 — 基础 — 函数(12):防止程序崩溃—— 宕机恢复(recover)