本文将会讲解defer, recover,panic相关的知识。主要内容包括:
- defer的原理
- panic与recover的原理及注意事项
其中重点在defer的原理。这部分包含了defer的定义、规则、实现原理、内部函数顺序四部分。
希望看完本文,你能对defer、recover、panic有个全面的认识~
一、defer的原理
定义
1、defer语句用于延迟函数的调用,每次defer都会把一个函数压入栈中,主函数(创建defer的函数)返回前再把延迟的函数取出并执行。defer最常见的场景是完成一些收尾的工作,比如文件句柄的关闭等。还有就是执行 recover, 实现类似其他语言中的try catch finally。
2、延迟函数可能有输入参数,这些参数可能来源于定义defer的函数,延迟函数也可能引用主函数用于返回的变量,也就是说延迟函数可能会影响主函数的一些行为。
规则
规则一、其实使用defer时,用一个简单的转换规则改写一下,就不会迷糊了。改写规则是将return语句拆成两句写,return xxx会被改写成:
返回值 = xxx
调用defer的函数
空的return
规则一的几个案例
下面通过一些案例进行总结:
题目一
func deferFuncParameter() {
var aInt = 1
defer fmt.Println(aInt)
aInt = 2
return
}
延迟函数fmt.Println(aInt) 在defer语句出现时就已经确定了,所以无论后面如何修改aInt的值都不会影响延迟函数。
上述程序转换之后是这样的:
func deferFuncParameter() {
var aInt = 1
anonymous = aInt // anonymous为匿名的变量
aInt = 2
fmt.Println(anonymous)
return
}
即使是结构体,也是传值,也不会影响。比如
type Test struct {
value int
}
func (t Test) print() {
println(t.value)
}
func main() {
test := Test{}
defer test.print()
test.value += 1
}
这段代码输出的也是0.
如果是结构体指针,则会影响输出。
type Test struct {
value int
}
func (t *Test) print() {
println(t.value)
}
func main() {
test := Test{}
defer test.print()
test.value += 1
}
这个输出的就是1, 因为传递的指针。
题目二
func printArray(array *[3]int) {
for i := range array {
fmt.Println(array[i])
}
}
func deferFuncParameter() {
var aArray = [3]int{1, 2, 3}
defer printArray(&aArray)
aArray[0] = 10
return
}
func main() {
deferFuncParameter()
}
函数deferFuncParameter定义了一个数组,通过defer调用printArray, 最后修改数组的第一个元素。printArray 函数接收数组的指针,即数组的地址,由于延迟函数执行时机在return语句之前,所以对数组的最终修改值被打印出来。
题目三
func deferFuncReturn() (result int) {
i := 1
defer func() {
result++
}()
return i
}
函数的return语句并不是原子的,实际执行分为设置返回值->ret。defer语句实际执行在主函数返回(ret)前,即拥有defer的函数返回过程是 : 设置返回值->执行defer->ret。所以return语句先把result设置为i的值,即1,defer语句中又把result递增1,所以最终返回的是2.
上述程序可以转换为
func deferFuncReturn() (result int) {
i := 1
result = i
func() {
result++
}()
return
}
总结上文的例子可以得出如下几个结论:
- 延迟函数的参数在defer语句出现时就已经确定下来了
如果是字面量,则肯定不受影响(如题目一); 如果是指针类型,规则仍然适用,只不过延迟函数的参数是一个地址值,这种情况下defer后面的语句对变量的修改可能会影响延迟函数(如题目二)。
- 延迟函数可能操作主函数的具名返回值
关键字return不是一个原子操作,实际上return只代理汇编指令ret,即将跳转程序执行。比如语句return i,实际上分两步进行,即将i值存入栈中作为返回值,然后执行跳转,而defer的执行时机正是跳转前,所以说defer执行时还是有机会操作返回值的。
规则二、从主函数返回值的角度看,有如下的几条规则:
1、主函数拥有匿名返回值,返回字面值
func foo() int {
var i int
defer func() {
i++
}()
return 1
}
一个主函数拥有一个匿名的返回值,返回时使用字面值,比如”1“, ”hello“这样的值,这种情况下defer是无法操作返回值的。
2、主函数拥有匿名返回值,返回变量
一个主函数拥有一个匿名的返回值,返回使用本地或全局变量,这种情况下defer语句可以引用到返回值,但不会改变返回值。
func foo() int {
var i int
defer func() {
i++
}()
return i
}
上面的函数,返回一个局部变量,同时defer函数也会操作这个局部变量。对于匿名返回值来说,可以假定仍然有一个变量存储返回值,假定返回值变量为"anony",上面的返回语句可以拆分成以下过程:
anony = i
i++
return
由于i是整型,会将值拷贝给anony,所以defer语句修改i值,对函数返回值不会造成影响。
3、主函数拥有具名返回值
主函数声明语句中带有名字的返回值,会被初始化一个局部变量,函数内部可以像使用局部变量一样使用该返回值。如果defer语句操作该返回值,可能会改变返回结果。
func foo() (ret int) {
defer func() {
ret++
}()
return 0}
上面的函数拆解之后是这样的:
ret = 0
ret++
return
defer实现原理
数据结构
每个goroutine数据结构中实际上也有一个defer指针,该指针指向一个defer的单链表,每次声明一个defer时就将defer插入到单链表表头,每次执行defer时就从单链表表头取出一个defer执行。
image.png
defer的创建和执行
源码包src/runtime/panic.go定义了两个方法分别用于创建defer和执行defer。
- deferproc(): 在声明defer处调用,其将defer函数存入goroutine的链表中;
- deferreturn():在return指令,准确的讲是在ret指令前调用,其将defer从goroutine链表中取出并执行。
可以简单这么理解,在编译在阶段,声明defer处插入了函数deferproc(),在函数return前插入了函数deferreturn()。
defer内部函数顺序
func TestDefer(t *testing.T) {
fmt.Println("a")
defer fmt.Println("b")
defer c()
defer d()
fmt.Println("f")
}
func c() {
fmt.Println("c")
}
func d() func(){
fmt.Println("d")
return func() {
fmt.Println("e")
}
}
输出为 a f d c b
结论:defer函数在函数执行结束后执行,若有多个defer函数,则执行顺序为后进先出。 主函数中,defer d() 并没有执行d()返回的闭包,所以结果里面并没有返回e.
func TestDefer(t *testing.T) {
fmt.Println("a")
defer fmt.Println("b")
defer c()
defer d()()
fmt.Println("f")
}
func c() {
fmt.Println("c")
}
func d() func(){
fmt.Println("d")
return func() {
fmt.Println("e")
}
}
这段代码只是在调用d方法时加了个括号,那么d方法返回的方法就会立即执行
返回结果为 a d f e c b 。 为什么不是 a f d e c b 呢? 这是因为在defer d()() 编译时,首先定义了函数d(), 此时就输出了d. 然后返回包含e的闭包函数。
即被defer标记的d函数中的程序“立即执行”,而d函数返回的函数则在测试方法结束后 按照“后进先出”的顺序执行。
再看一个 来自effective go的例子:
func trace(s string) string {
fmt.Println("entering:", s)
return s
}
func un(s string) {
fmt.Println("leaving:", s)
}
func a() {
defer un(trace("a"))
fmt.Println("in a")
}
func b() {
defer un(trace("b"))
fmt.Println("in b")
a()
}
func main() {
b()
}
结果会打印
entering: b
in b
entering: a
in a
leaving: a
leaving: b
没执行以前我以为是如下的结果:
in b
in a
entring: a
leaving: a
entring: b
leaving: b
为什么不对呢?
因为在编译时,b() 中的 defer un(trace("b")) ,是对un()函数的延迟,但是此时会执行trace("b")。
这个例子说明,defer标记的函数只是最外层的函数,如果defer标记函数的参数也是个函数,则作为参数的函数在编译时就会被执行了,不必等到defer标记函数执行时才执行。
二、panic与recover的原理及注意事项
- panic内置函数停止当前goroutine的正常执行,当函数F调用panic时,函数F的正常执行被立即停止,然后运行所有在F函数中的defer函数,然后F返回到调用他的函数对于调用者G,F函数的行为就像panic一样,终止G的执行并运行G中所defer函数,此过程会一直继续执行到goroutine所有的函数。panic可以通过内置的recover来捕获。
- recover内置函数用来管理含有panic行为的goroutine,recover运行在defer函数中,获取panic抛出的错误值,并将程序恢复成正常执行的状态。如果在defer函数之外调用recover,那么recover不会停止并且捕获panic错误如果goroutine中没有panic或者捕获的panic的值为nil,recover的返回值也是nil。由此可见,recover的返回值表示当前goroutine是否有panic行为
几个注意的问题
1、defer 表达式的函数如果定义在 panic 后面,该函数在 panic 后就无法被执行到
func main() {
panic("a")
defer func() {
fmt.Println("b")
}()
}
结果 b没有打印出来
而在defer后panic
func main() {
defer func() {
fmt.Println("b")
}()
panic("a")
}
结果b被正常打印。
2、F中出现panic时,F函数会立刻终止,不会执行F函数内panic后面的内容,但不会立刻return,而是调用F的defer,如果F的defer中有recover捕获,则F在执行完defer后正常返回,调用函数F的函数G继续正常执行
func G() {
defer func() {
fmt.Println("c")
}()
F()
fmt.Println("继续执行")
}
func F() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常:", err)
}
fmt.Println("b")
}()
panic("a")
}
结果
捕获异常: a
b
继续执行
c
3、如果F的defer中无recover捕获,则将panic抛到G中,G函数会立刻终止,不会执行G函数内后面的内容,但不会立刻return,而调用G的defer...以此类推
func G() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常:", err)
}
fmt.Println("c")
}()
F()
fmt.Println("继续执行")
}
func F() {
defer func() {
fmt.Println("b")
}()
panic("a")
}
结果
b
捕获异常: a
c
4、如果一直没有recover,抛出的panic到当前goroutine最上层函数时,程序直接异常终止
func G() {
defer func() {
fmt.Println("c")
}()
F()
fmt.Println("继续执行")
}
func F() {
defer func() {
fmt.Println("b")
}()
panic("a")
}
结果
b
c
panic: a
goroutine 1 [running]:
main.F()
/xxxxx/src/xxx.go:61 +0x55
main.G()
/xxxxx/src/xxx.go:53 +0x42
exit status 2
5、recover都是在当前的goroutine里进行捕获的,这就是说,对于创建goroutine的外层函数,如果goroutine内部发生panic并且内部没有用recover,外层函数是无法用recover来捕获的,这样会造成程序崩溃
func G() {
defer func() {
//goroutine外进行recover
if err := recover(); err != nil {
fmt.Println("捕获异常:", err)
}
fmt.Println("c")
}()
//创建goroutine调用F函数
go F()
time.Sleep(time.Second)
}
func F() {
defer func() {
fmt.Println("b")
}()
//goroutine内部抛出panic
panic("a")
}
结果:
b
panic: a
goroutine 5 [running]:
main.F()
/xxxxx/src/xxx.go:67 +0x55
created by main.main
/xxxxx/src/xxx.go:58 +0x51
exit status 2
6、recover返回的是interface{}类型而不是go中的 error 类型,如果外层函数需要调用err.Error(),会编译错误,也可能会在执行时panic
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常:", err.Error())
}
}()
panic("a")
}
编译错误,结果
err.Error undefined (type interface {} is interface with no methods)
func main() {
defer func() {
if err := recover(); err != nil {
fmt.Println("捕获异常:", fmt.Errorf("%v", err).Error())
}
}()
panic("a")
}
结果:
捕获异常: a
参考文献
Go defer实现原理剖析
理解 Go 语言 defer 关键字的原理
defer关键字
golang中的defer函数的执行顺序
go defer,panic,recover详解 go 的异常处理
effective_go中文版
谈谈 panic 和 recover 的原理,讲的比较深入
网友评论