GO语言中defer实现原理是什么
来源:亿速云
时间:2023-03-08 08:59:00 201浏览 收藏
对于一个Golang开发者来说,牢固扎实的基础是十分重要的,golang学习网就来带大家一点点的掌握基础知识点。今天本篇文章带大家了解《GO语言中defer实现原理是什么》,主要介绍了defer、go语言,希望对大家的知识积累有所帮助,快点收藏起来吧,否则需要时就找不到了!
这篇文章主要介绍“GO语言中defer实现原理是什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“GO语言中defer实现原理是什么”文章能帮助大家解决问题。
defer 是什么
咱们一起来看看 defer
是个啥
是 GO 中的一个关键字
这个关键字,我们一般用在释放资源,在 return
前会调用他
如果程序中有多个 defer
,defer 的调用顺序是按照类似栈的方式,后进先出 LIFO
的 ,这里顺便写一下
栈
遵循后进先出原则
后进入栈的,先出栈
先进入栈的,后出栈
队列
遵循先进先出 , 我们就可以想象一个单向的管道,从左边进,右边出
先进来,先出去
后进来,后出去,不准插队
defer 实现原理
咱们先抛出一个结论,先心里有点底:
代码中声明 defer
的位置,编译的时候会插入一个函数叫做 deferproc
,在该defer
所在的函数前插入一个返回的函数,不是return
哦,是deferreturn
具体的 defer
的实现原理是咋样的,我们还是一样的,来看看 defer
的底层数据结构是啥样的 ,
在 src/runtime/runtime2.go
的 type _defer struct {
结构
// A _defer holds an entry on the list of deferred calls. // If you add a field here, add code to clear it in freedefer and deferProcStack // This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct // and cmd/compile/internal/gc/ssa.go:(*state).call. // Some defers will be allocated on the stack and some on the heap. // All defers are logically part of the stack, so write barriers to // initialize them are not required. All defers must be manually scanned, // and for heap defers, marked. type _defer struct { siz int32 // includes both arguments and results started bool heap bool // openDefer indicates that this _defer is for a frame with open-coded // defers. We have only one defer record for the entire frame (which may // currently have 0, 1, or more defers active). openDefer bool sp uintptr // sp at time of defer pc uintptr // pc at time of defer fn *funcval // can be nil for open-coded defers _panic *_panic // panic that is running defer link *_defer // If openDefer is true, the fields below record values about the stack // frame and associated function that has the open-coded defer(s). sp // above will be the sp for the frame, and pc will be address of the // deferreturn call in the function. fd unsafe.Pointer // funcdata for the function associated with the frame varp uintptr // value of varp for the stack frame // framepc is the current pc associated with the stack frame. Together, // with sp above (which is the sp associated with the stack frame), // framepc/sp can be used as pc/sp pair to continue a stack trace via // gentraceback(). framepc uintptr }
_defer
持有延迟调用列表中的一个条目 ,我们来看看上述数据结构的参数都是啥意思
tag | 说明 |
---|---|
siz | defer函数的参数和结果的内存大小 |
fn | 需要被延迟执行的函数 |
_panic | defer 的 panic 结构体 |
link | 同一个协程里面的defer 延迟函数,会通过该指针连接在一起 |
heap | 是否分配在堆上面 |
openDefer | 是否经过开放编码优化 |
sp | 栈指针(一般会对应到汇编) |
pc | 程序计数器 |
defer 关键字后面必须是跟函数,这一点咱们要记住哦
通过上述参数的描述,我们可以知道,defer
的数据结构和函数类似,也是有如下三个参数:
栈指针 SP
程序计数器 PC
函数的地址
可是我们是不是也发现了,成员里面还有一个link
,同一个协程里面的defer 延迟函数,会通过该指针连接在一起
这个link
指针,是指向的一个defer
单链表的头,每次咱们声明一个defer
的时候,就会将该defer
的数据插入到这个单链表头部的位置,
那么,执行defer
的时候,我们是不是就能猜到defer
是咋取得了不?
前面有说到defer
是后进先出的,这里当然也是遵循这个道理,取defer
进行执行的时候,是从单链表的头开始去取的。
咱们来画个图形象一点
在协程A中声明2个defer
,先声明 defer test1()
再声明 defer test2()
可以看出后声明的defer
会插入到单链表的头,先声明的defer
被排到后面去了
咱们取的时候也是一直取头下来执行,直到单链表为空。
咱一起来看看defer 的具体实现
源码文件在 src/runtime/panic.go
中,查看 函数 deferproc
// Create a new deferred function fn with siz bytes of arguments. // The compiler turns a defer statement into a call to this. //go:nosplit func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn gp := getg() if gp.m.curg != gp { // go code on the system stack can't defer throw("defer on system stack") } // the arguments of fn are in a perilous state. The stack map // for deferproc does not describe them. So we can't let garbage // collection or stack copying trigger until we've copied them out // to somewhere safe. The memmove below does that. // Until the copy completes, we can only call nosplit routines. sp := getcallersp() argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc() d := newdefer(siz) if d._panic != nil { throw("deferproc: d.panic != nil after newdefer") } d.link = gp._defer gp._defer = d d.fn = fn d.pc = callerpc d.sp = sp switch siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp)) default: memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz)) } // deferproc returns 0 normally. // a deferred func that stops a panic // makes the deferproc return 1. // the code the compiler generates always // checks the return value and jumps to the // end of the function if deferproc returns != 0. return0() // No code can go here - the C return register has // been set and must not be clobbered. }
deferproc 的作用是
创建一个新的递延函数 fn
,参数为 siz 字节,编译器将一个延迟语句转换为对this
的调用
getcallersp()
:
得到deferproc
之前的rsp
寄存器的值,实现的方式所有平台都是一样的
//go:noescape func getcallersp() uintptr // implemented as an intrinsic on all platforms
callerpc := getcallerpc()
:
此处得到 rsp
之后,存储在 callerpc
中 , 此处是为了调用 deferproc
的下一条指令
d := newdefer(siz)
:
d := newdefer(siz)
新建一个defer
的结构,后续的代码是在给defer
这个结构的成员赋值
咱看看 deferproc 的大体流程
获取
deferproc
之前的rsp寄存器的值使用
newdefer
分配一个 _defer 结构体对象,并且将他放到当前的_defer
链表的头初始化_defer 的相关成员参数
return0
来我们看看 newdefer的源码
源码文件在 src/runtime/panic.go
中,查看函数newdefer
// Allocate a Defer, usually using per-P pool. // Each defer must be released with freedefer. The defer is not // added to any defer chain yet. // // This must not grow the stack because there may be a frame without // stack map information when this is called. // //go:nosplit func newdefer(siz int32) *_defer { var d *_defer sc := deferclass(uintptr(siz)) gp := getg() if sc 0 { d = pp.deferpool[sc][n-1] pp.deferpool[sc][n-1] = nil pp.deferpool[sc] = pp.deferpool[sc][:n-1] } } if d == nil { // Allocate new defer+args. systemstack(func() { total := roundupsize(totaldefersize(uintptr(siz))) d = (*_defer)(mallocgc(total, deferType, true)) }) } d.siz = siz d.heap = true return d }
newderfer
的作用:
通常使用per-P池,分配一个Defer
每个defer
可以自由的释放。当前defer
也不会加入任何一个 defer
链条中
getg()
:
获取当前协程的结构体指针
// getg returns the pointer to the current g. // The compiler rewrites calls to this function into instructions // that fetch the g directly (from TLS or from the dedicated register). func getg() *g
pp := gp.m.p.ptr()
:
拿到当前工作线程里面的 P
然后拿到 从全局的对象池子中拿一部分对象给到P的池子里面
for len(pp.deferpool[sc])点进去看池子的数据结构,其实里面的成员也就是 咱们之前说到的
_defer
指针其中
sched.deferpool[sc]
是全局的池子,pp.deferpool[sc]
是本地的池子mallocgc分配空间
上述操作若 d 没有拿到值,那么就直接使用
mallocgc
重新分配,且设置好 对应的成员siz
和heap
if d == nil { // Allocate new defer+args. systemstack(func() { total := roundupsize(totaldefersize(uintptr(siz))) d = (*_defer)(mallocgc(total, deferType, true)) }) } d.siz = siz d.heap = true
mallocgc
具体实现在src/runtime/malloc.go
中,若感兴趣的话,可以深入看看这一块,今天咱们不重点说这个函数// Allocate an object of size bytes. // Small objects are allocated from the per-P cache's free lists. // Large objects (> 32 kB) are allocated straight from the heap. func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {}最后再来看看return0
最后再来看看
deferproc
函数中的 结果返回return0()
// return0 is a stub used to return 0 from deferproc. // It is called at the very end of deferproc to signal // the calling Go function that it should not jump // to deferreturn. // in asm_*.s func return0()
return0
是用于从deferproc
返回0
的存根它在
deferproc
函数的最后被调用,用来通知调用Go
的函数它不应该跳转到deferreturn
。在正常情况下
return0
正常返回 0可是异常情况下
return0
函数会返回 1,此时GO 就会跳转到执行deferreturn
简单说下 deferreturn
deferreturn
的作用就是情况defer
里面的链表,归还相应的缓冲区,或者把对应的空间让GC
回收调GO 中 defer 的规则
上面分析了GO 中
defer
的实现原理之后,咱们现在来了解一下 GO 中应用defer
是需要遵守 3 个规则的,咱们来列一下:
defer
后面跟的函数,叫延迟函数,函数中的参数在defer
语句声明的时候,就已经确定下来了延迟函数的执行时按照后进先出来的,文章前面也多次说到过,这个印象应该很深刻吧,先出现的
defer
后执行,后出现的defer
先执行延迟函数可能会影响到整个函数的返回值
咱们还是要来解释一下的,上面第 2 点,应该都好理解,上面的图也表明了 执行顺序
第一点咱们来写个小DEMO
延迟函数中的参数在defer
语句声明的时候,就已经确定下来了
func main() { num := 1 defer fmt.Println(num) num++ return }
别猜了,运行结果是 1,小伙伴们可以将代码拷贝下来,自己运行一波
第三点也来一个DEMO
延迟函数可能会影响到整个函数的返回值
func test3() (res int) { defer func() { res++ }() return 1 } func main() { fmt.Println(test3()) return }
上述代码,我们在 test3
函数中的返回值,我们提前命名好了,本来应该是返回结果为 1
可是在return
这里,执行顺序这样的
res = 1
res++
因此,结果就是 2
关于“GO语言中defer实现原理是什么”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识,可以关注golang学习网行业资讯频道,小编每天都会为大家更新不同的知识点。
今天带大家了解了defer、go语言的相关知识,希望对你有所帮助;关于Golang的技术知识我们会一点点深入介绍,欢迎大家关注golang学习网公众号,一起学习编程~
-
366 收藏
-
485 收藏
-
185 收藏
-
444 收藏
-
461 收藏
-
165 收藏
-
473 收藏
-
377 收藏
-
384 收藏
-
246 收藏
-
110 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 542次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 507次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 497次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 484次学习