Go 基础知识与框架体系 系列六: defer, panic 和 recover
这篇文章总结了 Go 的知识体系 defer
, panic
和 recover
,包括其中的底层实现等等。
defer,panic 和 recover
1. defer
-
规则一:延迟函数的参数在
defer
语句出现时就已经确定下来了。例如:defer
语句中的fmt.Println()
参数i值在 defer 出现时就已经确定下来,实际上是拷贝了一份。后面对变量i的修改不会影响fmt.Println()
函数的执行,仍然打印"0"。
|
|
-
规则二:延迟函数执行按后进先出顺序执行,即先出现的
defer
最后执行。定义
defer
类似于入栈操作,执行defer
类似于出栈操作。设计defer
的初衷是简化函数返回时资源清理的动作,资源往往有依赖顺序,比如先申请A资源,再跟据A资源申请B资源,跟据B资源申请C资源,即申请顺序是:A–>B–>C,释放时往往又要反向进行。这就是把defer
设计成FIFO的原因。每申请到一个用完需要释放的资源时,立即定义一个defer
来释放资源是个很好的习惯。例如:函数拥有一个具名返回值
result
,函数内部声明一个变量i
,defer
指定一个延迟函数,最后返回变量i。延迟函数中递增result
。函数输出2。函数的
return
语句并不是原子的,实际执行分为设置返回值–>ret,defer
语句实际执行在返回前,即拥有defer
的函数返回过程是:设置返回值–>执行 defer–>ret。所以return
语句先把result
设置为i的值,即1,defer
语句中又把result
递增1,所以最终返回2。
|
|
-
规则三:延迟函数可能操作主函数的具名返回值。
定义
defer
的函数,即主函数可能有返回值,返回值有没有名字没有关系,defer
所作用的函数,即延迟函数可能会影响到返回值。例如我们再看一下上面deferFuncReturn()
的例子:
|
|
该函数的 return
语句可以拆分成下面两行:
|
|
而延迟函数的执行正是在 return
之前,即加入 defer
后的执行过程如下:
|
|
所以返回值为 result=1
。
2. panic
- 主动:程序猿主动调用
panic()
函数; - 被动:编译器的隐藏代码触发,或者内核发送给进程信号触发;
panic
的具体实现,是依靠 defer
指针处理的,我们先来看一看 panic
的结构体:
|
|
_panic
是个结构体,存储了 defer
指针、参数,panic
列表的表头指针,和已恢复或已终止的信息。以下是 panic
的处理流程:
-
- 每个 goroutine 都有一个
panic
链表,运行时,遇到panic
代码,会生成对应的_panic
数据,存到这个链表的表头。
- 每个 goroutine 都有一个
-
- 每执行完毕一个函数,如果没有
panic
发生,就跳过对应的_panic
数据,回到正常流程,否则进入3。
- 每执行完毕一个函数,如果没有
-
- 如果有
panic
发生,处理链表中对应的_panic
,进入4。
- 如果有
-
- 如果
defer
链表(跟panic
链表一样,也是每个 goroutine 一个)里存在defer
,按约定顺序执行延迟代码,进入5,否则进入8。
- 如果
-
- 当
defer
链表执行到需要recover
的时候,就交给reflectcall
去调用 gorecover,进入6,否则进入7。
- 当
-
- 执行
recover
,这时对应的_panic
结构里的 recovered 字段标记为真,由 recovery 方法,负责安抚当前的_panic
,回到正常流程。
- 执行
-
- 如果没
recover
,那就进入死给你看流程,进入8。
- 如果没
-
- 最后,执行
fatalpanic
方法。
- 最后,执行
注意:因为 golang 的 goroutine 机制,panic
在不同的 goroutine 里面,是单独的,并不是整体处理。可能一个地方凉了,就会整体完蛋,这个要非常小心。
3. recover
Golang 虽然没有 try catch
机制,但它有类似 recover
的机制,
|
|
在 cal
函数里面每次终止的时候都会检查有没有异常产生,如果产生了我们可以处理,比如说记录日志,这样程序还可以继续执行下去。