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专家编程》

相关文章
|
3月前
|
测试技术 Go 开发者
go-carbon v2.3.8 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
27 0
|
4月前
|
Go
golang数据结构篇之栈和队列以及简单标准库
golang数据结构篇之栈和队列以及简单标准库
35 0
|
4月前
|
测试技术 Go 开发者
go-carbon v2.3.7 发布,轻量级、语义化、对开发者友好的 golang 时间处理库
carbon 是一个轻量级、语义化、对开发者友好的 golang 时间处理库,支持链式调用。
19 2
|
3天前
|
运维 监控 Go
Golang深入浅出之-Go语言中的日志记录:log与logrus库
【4月更文挑战第27天】本文比较了Go语言中标准库`log`与第三方库`logrus`的日志功能。`log`简单但不支持日志级别配置和多样化格式,而`logrus`提供更丰富的功能,如日志级别控制、自定义格式和钩子。文章指出了使用`logrus`时可能遇到的问题,如全局logger滥用、日志级别设置不当和过度依赖字段,并给出了避免错误的建议,强调理解日志级别、合理利用结构化日志、模块化日志管理和定期审查日志配置的重要性。通过这些实践,开发者能提高应用监控和故障排查能力。
8 1
|
3天前
|
安全 Go
Golang深入浅出之-Go语言标准库中的文件读写:io/ioutil包
【4月更文挑战第27天】Go语言的`io/ioutil`包提供简单文件读写,适合小文件操作。本文聚焦`ReadFile`和`WriteFile`函数,讨论错误处理、文件权限、大文件处理和编码问题。避免错误的关键在于检查错误、设置合适权限、采用流式读写及处理编码。遵循这些最佳实践能提升代码稳定性。
9 0
|
5天前
|
中间件 Go API
Golang深入浅出之-Go语言标准库net/http:构建Web服务器
【4月更文挑战第25天】Go语言的`net/http`包是构建高性能Web服务器的核心,提供创建服务器和发起请求的功能。本文讨论了使用中的常见问题和解决方案,包括:使用第三方路由库改进路由设计、引入中间件处理通用逻辑、设置合适的超时和连接管理以防止资源泄露。通过基础服务器和中间件的代码示例,展示了如何有效运用`net/http`包。掌握这些最佳实践,有助于开发出高效、易维护的Web服务。
20 1
|
9天前
|
Go 开发者
Golang深入浅出之-Go语言 defer、panic、recover:异常处理机制
Go语言中的`defer`、`panic`和`recover`提供了一套独特的异常处理方式。`defer`用于延迟函数调用,在返回前执行,常用于资源释放。它遵循后进先出原则。`panic`触发运行时错误,中断函数执行,直到遇到`recover`或程序结束。`recover`在`defer`中捕获`panic`,恢复程序执行。注意避免滥用`defer`影响性能,不应对可处理错误随意使用`panic`,且`recover`不能跨goroutine捕获panic。理解并恰当使用这些机制能提高代码健壮性和稳定性。
19 2
|
3月前
|
存储 缓存 算法
Golang高性能内存缓存库BigCache设计与分析
【2月更文挑战第4天】分析Golang高性能内存缓存库BigCache设计
73 0
|
4月前
|
Java Go
浅谈Golang 不同版本的defer
浅谈Golang 不同版本的defer
28 1
|
4月前
|
存储 编译器 测试技术
Golang底层原理剖析之defer
Golang底层原理剖析之defer
16 0