数据结构
defer的数据结构定义在$GOROOT/src/runtime/runtime2.go
// 大体定义如下,忽略少部分字段type _deferstruct{
spuintptr//函数栈指针
pcuintptr//程序计数器
fn*funcval//函数地址
link*_defer//指向自身结构的指针, 用于链接多个defer}
规则约定
- 规则一: 延迟函数的参数在defer语句出现时就已经确定
- 规则二: 延迟函数执行按后进先出顺序执行, 即先出现的defer最后执行
- 规则三: 延迟函数可能操作主函数的具名返回值
实现原理
案例
先来看一个简单的demo代码,在TestDefer()
方法中声明了一个defer
函数,我们在defer函数前、中、后都对值s
进行了修改操作,下面通过这个小案例看看它会产生什么东西以及它内部的执行和实现逻辑是怎样的。
5funcmain(){6 s:=TestDefer()78 fmt.Println("result => ",s)9}1011funcTestDefer()(resultstring){12 s:="init"1314deferfunc(sstring){15 s="defer2"16}(s)1718deferfunc(sstring){19 s="defer1"20}(s)2122 s="return"2324return s25}
通过go tool objdump -s ‘main’ [program]
来查看代码的反编译情况,如下:
TEXT main.TestDefer(SB)/Users/guanjian/workspace/go/program/defer/defer.go//省略......defer.go:140x10cbd75 e8a6b3f6ff CALL runtime.deferprocStack(SB)//省略......defer.go:180x10cbdc1 e85ab3f6ff CALL runtime.deferprocStack(SB)//省略......defer.go:240x10cbe00 e83bbbf6ff CALL runtime.deferreturn(SB)//省略......defer.go:180x10cbe16 e825bbf6ff CALL runtime.deferreturn(SB)//省略......defer.go:140x10cbe2c e80fbbf6ff CALL runtime.deferreturn(SB)//省略......defer.go:140x10cbe40 c3 RETdefer.go:110x10cbe41 e8fa11faff CALL runtime.morestack_noctxt(SB)defer.go:110x10cbe46 e995feffff JMP main.TestDefer(SB)//省略......
defer初始
runtime.deferprocStack
在声明defer处调用, 将defer函数存入goroutine的链表中,这相当于defer的初始阶段,若defer声明中包含参数,那么参数将被初始化且不会受到后续变更影响,也就是说defer的初始化阶段入参只能初始化赋值。
defer执行
runtime.deferreturn
在return指令, 准确的讲是在ret指令前调用,将defer从goroutine链表中取出并执行,这相当于defer的执行阶段。
通过反编译文件来看,是按照如下顺序执行的:
多个defer的执行顺序 :
通过梳理可知defer执行顺序是根据声明顺序的倒序执行的,即声明顺序为defer-1、defer-> > 2、defer-3,那么执行顺序则为defer-3、defer-2、defer-1,他们是通过_defer
的link *_defer
进行串联的。
案例分析
下面有四个方法,test_01、test_02是值传递,test_03、test_04是引用传递
我们分别从作用域、栈执行顺序、值传递等来分析下
案例-1
funcmain(){
s1:=test_01()
fmt.Println("test_01 => ", s1)//test_01 => 0}functest_01()int{var iintdeferfunc(){
fmt.Println("defer-2 => ",i)//这里打印1,上一个defer操作生效了
i++//不影响返回值}()deferfunc(){
fmt.Println("defer-1 => ",i)//这里打印0
i++//不影响返回值}()return i//返回值以此为准}
案例-2
funcmain(){
s2:=test_02()
fmt.Println("test_02 => ", s2)//test_02 => 2}functest_02()(resint){var iintdeferfunc(){
fmt.Println("defer-2 => ",res)//这里打印1,上一个defer操作生效了
res++//影响返回值,最终返回值}()deferfunc(){
fmt.Println("defer-1 => ",res)//这里打印0
res++//影响返回值}()return i//影响返回值,但并不是唯一和最终影响返回值的地方}
案例-3
funcmain(){
s3:=test_03()
fmt.Println("test_03 => ", s3)//test_03 => &{jack}}type Userstruct{
Namestring}functest_03()*User{
user:=&User{}deferfunc(){
user.Name="jack"//不影响返回值}()return user//返回值以此为准}
案例-4
funcmain(){
s4:=test_04()
fmt.Println("test_04 => ", s4)//test_04 => &{jack}}type Userstruct{
Namestring}functest_04()(resUser*User){
user:=&User{}deferfunc(){
resUser.Name="jack"//影响返回值,返回值以此为准}()return user//影响返回值,但并不是唯一和最终影响返回值的地方}
案例4和案例3的逻辑基本一样,可参考案例3
总结
- defer定义的延迟函数参数在defer语句出时就已经确定下来了
- defer定义顺序与实际执行顺序相反
- return不是原子操作, 执行过程是: 保存返回值(若有)—>执行defer( 若有) —>执行ret跳转
- 申请资源后立即使用defer关闭资源是好习惯
参考
《Go专家编程》