Golang标准库揭秘系列 | defer详解

简介: golang defer详解


theme: smartblue


数据结构


defer的数据结构定义在$GOROOT/src/runtime/runtime2.go


// 大体定义如下,忽略少部分字段

type _defer struct {

sp uintptr //函数栈指针

pc uintptr //程序计数器

fn *funcval //函数地址

link *_defer //指向自身结构的指针, 用于链接多个defer

}


规则约定


  • 规则一: 延迟函数的参数在defer语句出现时就已经确定
  • 规则二: 延迟函数执行按后进先出顺序执行, 即先出现的defer最后执行
  • 规则三: 延迟函数可能操作主函数的具名返回值


实现原理


案例


先来看一个简单的demo代码,在TestDefer()方法中声明了一个defer函数,我们在defer函数前、中、后都对值s进行了修改操作,下面通过这个小案例看看它会产生什么东西以及它内部的执行和实现逻辑是怎样的。


5 func main() {

6  s := TestDefer()

7

8  fmt.Println("result => ",s)

9 }

10

11 func TestDefer()(result string)  {

12  s := "init"

13

14  defer func(s string) {

15   s = "defer2"

16  }(s)

17

18  defer func(s string) {

19   s = "defer1"

20  }(s)

21

22  s = "return"

23

24  return s

25 }


通过go tool objdump -s ‘main’ [program]来查看代码的反编译情况,如下:


TEXT main.TestDefer(SB) /Users/guanjian/workspace/go/program/defer/defer.go

//省略......                      

 defer.go:14           0x10cbd75               e8a6b3f6ff                      CALL runtime.deferprocStack(SB)        

//省略......                        

 defer.go:18           0x10cbdc1               e85ab3f6ff                      CALL runtime.deferprocStack(SB)        

//省略......                            

 defer.go:24           0x10cbe00               e83bbbf6ff                      CALL runtime.deferreturn(SB)            

//省略......                                    

 defer.go:18           0x10cbe16               e825bbf6ff                      CALL runtime.deferreturn(SB)            

//省略......                                  

 defer.go:14           0x10cbe2c               e80fbbf6ff                      CALL runtime.deferreturn(SB)            

//省略......                      

 defer.go:14           0x10cbe40               c3                              RET                                    

 defer.go:11           0x10cbe41               e8fa11faff                      CALL runtime.morestack_noctxt(SB)      

 defer.go:11           0x10cbe46               e995feffff                      JMP main.TestDefer(SB)                  

//省略......


defer初始


runtime.deferprocStack 在声明defer处调用, 将defer函数存入goroutine的链表中,这相当于defer的初始阶段,若defer声明中包含参数,那么参数将被初始化且不会受到后续变更影响,也就是说defer的初始化阶段入参只能初始化赋值。


defer执行


runtime.deferreturnreturn指令, 准确的讲是在ret指令前调用,将defer从goroutine链表中取出并执行,这相当于defer的执行阶段


通过反编译文件来看,是按照如下顺序执行的:


网络异常,图片无法展示
|


多个defer的执行顺序 :

通过梳理可知defer执行顺序是根据声明顺序的倒序执行的,即声明顺序为defer-1、defer-> > 2、defer-3,那么执行顺序则为defer-3、defer-2、defer-1,他们是通过_deferlink *_defer进行串联的。


案例分析


下面有四个方法,test_01、test_02是值传递,test_03、test_04是引用传递
我们分别从作用域、栈执行顺序、值传递等来分析下


案例-1


func main() {

s1 := test_01()


fmt.Println("test_01 => ", s1) //test_01 =>  0

}


func test_01() int {

var i int

defer func() {

fmt.Println("defer-2 => ",i)//这里打印1,上一个defer操作生效了

i++ //不影响返回值

}()

defer func() {

fmt.Println("defer-1 => ",i)//这里打印0

i++ //不影响返回值

}()

return i //返回值以此为准

}


网络异常,图片无法展示
|


案例-2


func main() {

s2 := test_02()

 

fmt.Println("test_02 => ", s2) //test_02 =>  2

}


func test_02() (res int) {

var i int

defer func() {

fmt.Println("defer-2 => ",res)//这里打印1,上一个defer操作生效了

res++ //影响返回值,最终返回值

}()

defer func() {

fmt.Println("defer-1 => ",res)//这里打印0

res++ //影响返回值

}()

return i //影响返回值,但并不是唯一和最终影响返回值的地方

}


网络异常,图片无法展示
|


案例-3


func main() {

s3 := test_03()

fmt.Println("test_03 => ", s3) //test_03 =>  &{jack}

}


type User struct {

Name string

}


func test_03() *User {

user := &User{}

defer func() {

user.Name="jack" //不影响返回值

}()

return user //返回值以此为准

}


网络异常,图片无法展示
|


案例-4


func main() {

s4 := test_04()


fmt.Println("test_04 => ", s4) //test_04 =>  &{jack}

}


type User struct {

Name string

}


func test_04() (resUser *User) {

user := &User{}

defer func() {

resUser.Name="jack" //影响返回值,返回值以此为准

}()

return user //影响返回值,但并不是唯一和最终影响返回值的地方

}


案例4案例3的逻辑基本一样,可参考案例3


总结


  • defer定义的延迟函数参数在defer语句出时就已经确定下来了
  • defer定义顺序与实际执行顺序相反
  • return不是原子操作, 执行过程是: 保存返回值(若有)—>执行defer( 若有) —>执行ret跳转
  • 申请资源后立即使用defer关闭资源是好习惯


参考


《Go专家编程》

相关文章
|
17天前
|
存储 Cloud Native Shell
go库介绍:Golang中的Viper库
Viper 是 Golang 中的一个强大配置管理库,支持环境变量、命令行参数、远程配置等多种配置来源。本文详细介绍了 Viper 的核心特点、应用场景及使用方法,并通过示例展示了其强大功能。无论是简单的 CLI 工具还是复杂的分布式系统,Viper 都能提供优雅的配置管理方案。
|
1月前
|
Go Python
通过 atexit 模块让 Python 实现 Golang 的 defer 功能
通过 atexit 模块让 Python 实现 Golang 的 defer 功能
20 2
|
2月前
|
Unix Go
Golang语言标准库time之日期和时间相关函数
这篇文章是关于Go语言日期和时间处理的文章,介绍了如何使用Go标准库中的time包来处理日期和时间。
48 3
|
3月前
|
JSON Go API
一文搞懂 Golang 高性能日志库 - Zap
一文搞懂 Golang 高性能日志库 - Zap
231 2
|
3月前
|
Serverless Go
Golang 开发函数计算问题之defer 中的 recover() 没有捕获到 如何解决
Golang 开发函数计算问题之defer 中的 recover() 没有捕获到 如何解决
|
3月前
|
存储 JSON Go
一文搞懂 Golang 高性能日志库 Zerolog
一文搞懂 Golang 高性能日志库 Zerolog
351 0
|
3月前
|
人工智能 数据库连接 Go
golang defer 详解
golang defer 详解
47 0
|
3月前
|
JSON Go 数据格式
[golang]标准库-json
[golang]标准库-json
|
5月前
|
SQL NoSQL Go
技术经验分享:Golang标准库:errors包应用
技术经验分享:Golang标准库:errors包应用
41 0
|
6月前
|
Go
Golang标准库sync的使用
Golang标准库sync的使用
58 2