内容简介:本文将会讲解defer, recover,panic相关的知识。主要内容包括:其中重点在defer的原理。这部分包含了defer的定义、规则、实现原理、内部函数顺序四部分。希望看完本文,你能对defer、recover、panic有个全面的认识~
本文将会讲解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-recover-panic 学习》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 一文读懂监督学习、无监督学习、半监督学习、强化学习这四种深度学习方式
- 学习:人工智能-机器学习-深度学习概念的区别
- 统计学习,机器学习与深度学习概念的关联与区别
- 混合学习环境下基于学习行为数据的学习预警系统设计与实现
- 学习如何学习
- 深度学习的学习历程
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。