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:\Go\src\Go36\article21\example02> 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:\Go\src\Go36\article21\example02>
到这里, 应该已经对 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\n", 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]\n", i)
- }
- defer fmt.Println("last defer")
- }
来源: http://www.bubuko.com/infodetail-2928479.html