一、前言

我们的应用程序常常会出现异常,包括由运行时检测到的异常或者应用开发者自己抛出的异常。

  • 异常在一些其他语言中,如c++、java,被叫做Exception,主要由抛出异常和捕获异常两部分组成。
  • 异常在go语言中,叫做panic,且由panic和recover方法组成,panic用来抛出,recover用来从panic中恢复。

1.1 panic实例分析

以下是一段简单的panic和recover使用示例:

package mainimport "fmt"func main() {f()fmt.Println("Returned normally from f.")
}func f() {/*defer func() {if r := recover(); r != nil {fmt.Println("Recovered in f", r)}}()*/fmt.Println("Calling g.")g(0)fmt.Println("Returned normally from g.")
}func g(i int) {fmt.Println("Printing in g", i)panic(i)fmt.Println("After panic in g", i)
}

我们先把defer recover部分注释,运行结果如下:

Calling g.
Printing in g 0
panic: 0goroutine 1 [running]:
main.g(0x4b14a0)/tmp/sandbox2444947193/prog.go:18 +0x94
main.f()/tmp/sandbox2444947193/prog.go:12 +0x5d
main.main()/tmp/sandbox2444947193/prog.go:6 +0x19Program exited.

可以看到程序运行到g方法的第二行时,产生的panic导致进程异常退出,后续的代码都没有执行。

再把recover注释部分打开,运行结果为:

Calling g.
Printing in g 0
Recovered in f 0
Returned normally from f.Program exited.

f方法中的recover捕获了panic,打印了panic传递的参数,并且main方法是正常返回的。g方法panic之后的代码没有执行。

1.2 官方翻译

panic是go的内置函数,它可以终止程序的正常执行流程并发出panic。

比如:当函数F调用panic,F的执行将被终止,并返回到调用者。对调用者而言,F就像调用者直接调用了panic。该过程一直跟随堆栈向上,直到当前goroutine中的所有函数都返回,此时 程序崩溃
panic可以通过直接调用panic产生。同时也可能由运行时的错误所产生,例如数组越界访问。

recover是go语言的内置函数,它的唯一作用是可以从panic中重新控制goroutine的执行。recover必须通过defer来运行

在正常的执行流程中,调用recover将会返回nil且没有什么其他的影响。但是如果当前的goroutine产生了panic,recover将会捕获到panic抛出的信息,同时恢复其正常的执行流程。

小结

  1. panic可以令程序崩溃(异常退出)
  2. recover可以让程序从panic中恢复,并正常运行
  3. 即使单个goroutine中发生了panic,也会使整个进程崩溃
  4. recover必须通过defer来运行

二、实现原理

2.1 panic从哪来

我们可以手动调用内置函数panic,但是那些空指针、数组越界等运行时panic是如何被检测到的,下面针对这一问题做一些代码调试

2.1.1 常见的几种panic

  • 空指针 invalid memory address or nil pointer dereference
  • 数组越界 index out of range;slice bounds out of range
  • 除数为零 integer divide by zero
  • 自定义panic

2.1.2 追踪panic来源

测试代码

package main
func main() {a := 0testDivide(a) //除零//testOutRange() //越界//testNil() //空指针//panic("666") //自定义panic
}
func testDivide(a int) {b := 10 / a_ = b
}
func testOutRange() {var a []inta[0] = 2
}
func testNil() {var a *int*a = 1
}

调试代码

与linux平台下的gdb调试工具类似,dlv用来调试go语言编写的程序。

dlv是一个命令行工具,它包含了多个调试命令,例如运行程序、下断点、打印变量、step in、step out等。我们常用的go语言编辑器,如vscode、golang等的可视化调试也是调用dlv。

找出panic是怎么产生的:

这里我们先给出结论,具体调试过程产生的代码,请往下看

调试自定义panic方法:

  1. 在8行处下断点
  2. 打印main方法的汇编代码
  3. 可以看到panic方法编译后实质是runtime包中的gopanic方法

使用dlv调试testDivide中的代码,有以下几个关键步骤:

  1. 在12行处下断点
  2. 打印testDivide方法的汇编代码
  3. testDivide方法中测试参数a的值是否为零
  4. 如果为零,则调用runtime包的panicdivide方法
  5. 调用runtime包的panicdivide方法
  6. panicdivide方法调用了panic
  7. 打印panicdivide的汇编代码,panic方法编译后实质是runtime包中的gopanic方法

所以其实panic方法实际调用了runtime.gopanic

  • 编译后的testDivide方法中除了正常的除法逻辑,编译器塞入了判断除数是否为零的代码分支,当除数为零则进入panic流程,与自定义panic相同,同样调用了runtime.gopanic
  • 其他数组越界及空指针,也都是调用了runtime.gopanic进入panic流程,不同的是:数组越界与除数为零相似,是通过编译器塞入判断分支进行越界检测;而空指针是通过访问非法地址产生中断进入panic流程。

小结

  • panic可以由开发者调用内置函数抛出
  • 编译器将检测异常的代码加入到程序中,会出现异常时抛出
  • 某些非法指令产生中断,并由中断处理函数抛出

2.2 panic到哪去

2.2.1 panic后的处理流程

由于panic和defer有着难解难分的关系,我们先了解一下defer。

defer定义的官翻:

defer语句将函数调用保存到一个列表上。保存的调用列表在当前函数返回前执行。Defer通常用于简化执行各种清理操作的函数。

通俗地说,就是defer保证函数调用不管在什么情况下(即使当前函数发生panic),在当前函数返回前必然执行。另外defer的函数调用符合先进后出的规则,即先defer的函数后执行。

我们看一个示例程序,它是第一节示例程序的升级版本,方法g中会调用自身:

package mainimport "fmt"func main() {defer func() {fmt.Println("defer in main")}()f()fmt.Println("Returned normally from f.")
}func f() {/*defer func() {if r := recover(); r != nil {fmt.Println("Recovered in f", r)}}()*/defer func() {fmt.Println("defer in f")}()fmt.Println("Calling g.")g(0)fmt.Println("Returned normally from g.")
}func g(i int) {if i > 3 {fmt.Println("Panicking!")panic(fmt.Sprintf("%v", i))}defer fmt.Println("Defer in g", i)fmt.Println("Printing in g", i)g(i + 1)
}

程序运行结果如下:

Calling g.
Printing in g 0
Printing in g 1
Printing in g 2
Printing in g 3
Panicking!
Defer in g 3
Defer in g 2
Defer in g 1
Defer in g 0
defer in f
defer in main
panic: 4goroutine 1 [running]:
main.g(0x4)/tmp/sandbox2114608904/prog.go:30 +0x1ec
main.g(0x3)/tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x2)/tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x1)/tmp/sandbox2114608904/prog.go:34 +0x136
main.g(0x0)/tmp/sandbox2114608904/prog.go:34 +0x136
main.f()/tmp/sandbox2114608904/prog.go:23 +0x7f
main.main()/tmp/sandbox2114608904/prog.go:9 +0x3fProgram exited

从运行结果可以观察到defer的作用,即使方法g中当i为4时发生了panic,每个defer的函数调用依然正常被执行了,而且是先进后出的顺序被执行。就像是每次defer时,将被defer的函数调用push到一个栈数据结构中,当返回时,再从栈中挨个将defer的函数pop出来并执行。

recover函数调用必须使用defer关键字,就是因为defer的函数调用必然会被执行。可以将以上实例中defer recover部分打开观察输出,与第一节中defer recover输出类似,程序可以正常执行并正常退出。

2.2.2 源码分析

我们再对源码做一下简单分析,以加深对panic及recover处理流程的理解。

首先简单了解下有关defer的一对方法:deferproc和deferreturn。

  • deferproc即defer关键字的实现,它将defer的函数调用push到当前goroutine中的defer链表头部
  • deferreturn,当一个函数中包含defer操作,编译器将在函数返回前插入一条deferreturn调用,deferreturn会将当前函数中defer的函数调用依次执行完毕

panic方法对应的实现为runtime.gopanic,recover方法对应的实现为runtime.gorecover。

源码如下(为了简化理解,省略了很多分支判断,只保留主流程的代码):

func gopanic(e interface{}) {//获取当前goroutine的对象gpgp := getg()...//将当前panic添加到gp的panic链表头部var p _panicp.arg = ep.link = gp._panicgp._panic = (*_panic)(noescape(unsafe.Pointer(&p)))...//循环执行defer链表中的函数for {//获取gp的defer链表d := gp._deferif d == nil {//如果没有defer,退出循环break}...done := true...//执行defer的函数调用var regs abi.RegArgsreflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz), uint32(d.siz), &regs)...p.argp = nild._panic = nil...if done {//清理defer对象,并设置下一个defer对象到gp的defer链表头部d.fn = nilgp._defer = d.linkfreedefer(d)}if p.recovered {//如果defer运行了recover函数,调用内置的recovery函数恢复调用//recovery函数会将当前的调用栈改变到deferreturn,从而使得程序可以继续正常运行...gp.sigcode0 = uintptr(sp)gp.sigcode1 = pcmcall(recovery)throw("recovery failed") // mcall should not return}}//如果没有recover,defer执行完毕,打印panic信息,并退出进程preprintpanics(gp._panic)fatalpanic(gp._panic) // should not return*(*int)(nil) = 0      // not reached
}//recover方法的实现
func gorecover(argp uintptr) interface{} {gp := getg()p := gp._panic...//recover方法仅有的一个作用,将recovered置为truep.recovered = truereturn p.arg
}

小结

  • panic处理过程中会检测是否有defer的函数调用
  • 如果有,按照先进后出的顺序依次执行
  • 如果defer中有recover调用,则将调用栈修改到deferreturn,使得程序正常执行
  • 否则当defer的函数调用执行完后,打印panic信息,进程退出

2.3 panic 打印信息

最后我们通过一个简单的例子,看一下recover后如何打印panic信息,及如何阅读panic信息

示例是一个除零的panic:

  1. recover后,调用printPanicInfo方法
  2. printPanicInfo使用runtime.Stack方法收集调用堆栈信息
  3. r为recover返回的参数,即panic传入的参数,一般为panic的具体原因,本示例为:runtime error: integer divide by zero
  4. 将panic原因和堆栈信息拼接并打印
package main
import ("fmt""runtime"
)
func main() {f()
}
func f() {defer func() {if r := recover(); r != nil {printPanicInfo(r)}}()g()
}
func g() {a := 10var b inta = a / b
}
func printPanicInfo(r interface{}) {buf := make([]byte, 64<<10)buf = buf[:runtime.Stack(buf, false)]s := fmt.Sprintf("%s\n%s", r, buf)fmt.Println(s)
}

输出为:

//panic的原因
runtime error: integer divide by zero
//goroutine的id
goroutine 1 [running]:
//下面是runtime.Stack方法调用时的调用堆栈链,方法名称和方法被调用的文件行数成对出现
main.printPanicInfo(0x4b78c0, 0x572a10) //方法名称E:/xxx/liuwei/test/main.go:29 +0x74 //方法所在的文件和行数
main.f.func1()E:/xxx/liuwei/test/main.go:15 +0x59
panic(0x4b78c0, 0x572a10)C:/go1.13/go/src/runtime/panic.go:679 +0x1c0 //panic被调用
main.g(...)E:/xxx/liuwei/test/main.go:24 //发生panic的代码行数
main.f()E:/xxx/liuwei/test/main.go:18 +0x50
main.main()E:/xxx/liuwei/test/main.go:9 +0x27

打印的信息中主要由panic原因调用堆栈组成,我们阅读堆栈信息时,可以首先找到runtime.panic,它的下一条堆栈记录就是发生panic的代码具体行数。然后再结合panic的原因信息,一般会很快了解到panic发生的原因。

另外除了panic之外还有一种fatalpanic,这种严重的异常无法使用recover恢复,一般是运行时检测到不可恢复的操作时抛出。例如发生map并发写时会throw(“concurrent map writes”),导致进程崩溃。

特别提示

  1. 因为Golang的gorotuine机制,panic在不同的gorotuine里面,是单独的,并不是整体处理。可能一个地方挂了,就会整体挂掉,这个要非常小心。

三、总结

  1. panic() 会退出进程,是因为调用了 exit 的系统调用;
  2. recover() 并不是说只能在 defer 里面调用,而是只能在 defer 函数中才能生效,只有在 defer 函数里面,才有可能遇到 _panic 结构;
  3. recover() 所在的 defer 函数必须和 panic 都是挂在同一个 goroutine 上,不能跨协程,因为 gopanic 只会执行当前 goroutine 的延迟函数;
  4. panic 的恢复,就是重置 pc 寄存器,直接跳转程序执行的指令,跳转到原本 defer 函数执行完该跳转的位置(deferreturn 执行),从 gopanic 函数中跳出,不再回来,自然就不会再 fatalpanic;
  5. panic 为啥能嵌套?这个问题就像是在问为什么函数调用可以嵌套一样,因为这个本质是一样的。

参考资料
6. 深度细节 | Go 的 panic 秘密都在这
7. go panic 的实现原理

Go panic的学习相关推荐

  1. Go 学习笔记(19)— 函数(05)[如何触发 panic、触发 panic 延迟执行、panic 和 recover 的关系]

    1. 异常设计思想 Go 语言的错误处理思想及设计包含以下特征: 一个可能造成错误的函数,需要返回值中返回一个错误接口( error ),如果调用是成功的,错误接口将返回 nil ,否则返回错误. 在 ...

  2. Go学习——defer、panic

    defer: 延迟到ret之前,通常用于IO的关闭 or 错误处理. 在延迟出现的异常可以被后面的捕捉,但是只有最后一个. defer可以多次,这样形成一个defer栈,后defer的语句在函数返回时 ...

  3. Go语言学习 二十三 错误处理和运行时恐慌(Panic)

    本文最初发表在我的个人博客,查看原文,获得更好的阅读体验 一 错误 1.1 error类型 按照约定,Go中的错误类型为error,这是一个内建接口,nil值表示没有错误: type error in ...

  4. panic函数c语言,【go语言学习】错误error和异常panic

    一.错误和异常的区别 错误指的是可能出现问题的地方出现了问题.比如打开一个文件时失败,这种情况在人们的意料之中 . 异常指的是不应该出现问题的地方出现了问题.比如引用了空指针,这种情况在人们的意料之外 ...

  5. 我的RUST学习——【第九章 9-1】panic! 与不可恢复的错误

    突然有一天,代码出问题了,而你对此束手无策.对于这种情况,Rust 有 panic! 宏.当执行这个宏时,程序会打印出一个错误信息,展开并清理栈数据,然后接着退出.出现这种情况的场景通常是检测到一些类 ...

  6. go学习之异常记录01:panic: reflect: call of reflect.Value.NumField on int Value

    代码: package mainimport ("fmt""reflect""strconv""strings" )ty ...

  7. golang学习笔记之panic recover

    golang异常捕获后,可以记录堆栈信息到日志,方便以后分析,同时异常捕获后,主程序可以继续运行: recover初级用法 示例代码如下: package mainimport ("fmt& ...

  8. go-kit微服务学习-官方示例stringsvc学习

    kit库 该库详细的文档可以参考官方文档,本文只是针对kit官网给出的stringsvc相关例子示例的学习. 示例代码stringsvc1 package mainimport ("cont ...

  9. golang库context学习

    context库 context最早的背景说明还是来源于官方的 博客,说明如下: 在Go服务器中,每个传入请求都在其自己的goroutine中进行处理. 请求处理程序通常会启动其他goroutine来 ...

最新文章

  1. 2021年大数据常用语言Scala(四):基础语法学习 声明变量
  2. python基础知识选择题-Python练习题(基础知识练习题(一))
  3. 《深入Java虚拟机》笔记
  4. 最好用的日志分析工具ELK
  5. 关于python多线程和定时器 看图不说话
  6. 电脑装oracle服务很卡,oracle11g安装后电脑启动很慢怎么解决
  7. KNN(六)--LSH算法
  8. 自定义网页头部前面小图标
  9. markdown html 注释,在 Markdown 注释
  10. mysql漏洞如何打补丁_WordPress 5.1 CSRF to RCE 漏洞详解
  11. cpu性能参数如何看?
  12. 俄亥俄州立大学哥伦布分校计算机科学,美国俄亥俄州立大学哥伦布分校计算机科学与工程硕士专业入学要求精选.pdf...
  13. 【技术文档】jeecg3.8-maven 开发环境搭建入门
  14. Rust - Pin | Unpin | PhantomPinned
  15. java pdf模板填充生成pdf打印 (亲测有效)
  16. java开发名言_java实现收藏名言语句台词的app
  17. matlab 工具箱下载地址
  18. 在线K歌又现新模式 音遇APP能否站稳脚跟?
  19. 密码学基础算法(二)中国剩余定理
  20. 抖音怎么创建共创抖音共创是什么?怎么操作全集教程

热门文章

  1. 神经网络—卷积神经网络CNN
  2. RISC-V架构P扩展指令集的研究与实现(一)
  3. 【SVM之菜鸟实现】—5步SVM
  4. log4cpp-【写日志】:使用log4cpp的基本步骤
  5. jadx 反编译apk
  6. c语言程序24转换12时间,C语言将24小时制转换为12小时制的方法
  7. exit abort return 区别
  8. 数据挖掘学习——聚类分析(k-均值聚类、DBSCAN、AGNES)、python代码
  9. 计算机一级vlookup函数的使用方法,电子档Excel中vlookup函数的使用方法(图解详细说明)...
  10. 【JMS】JMS支持的模式讲解