遂宁市网站建设_网站建设公司_安全防护_seo优化
2026/1/19 18:52:17 网站建设 项目流程

文章目录

  • defer是什么​
  • defer的使用形式
  • defer的底层结构
  • defer的执行过程
    • _defer内存分配
      • 堆上分配
      • 栈上分配
      • 开放编码
    • defer函数执行
  • defer面试与分析
  • 1、defer的底层数据结构是怎样的​
  • 2、循环体中能用defer调用吗? 会有什么问题,为什么?
  • 3、defer能修改返回值吗,defer与return的先后关系是怎样的?
  • 4、多个defer的执行顺序是怎样的?

defer是什么​

defer是go语言的一个关键字,用来修饰函数,其作用是让defer后面跟的函数或者方法调用能够延迟到当前所在函数return或者panic的时候再执行。

defer的使用形式

deferfunc(args)

defer在使用的时候,只需要在其后面加上具体的函数调用即可,这样就会注册一个延迟执行的函数func,并且会把函数名和参数都确定,等到从当前函数退出的时候再执行

defer的底层结构

进行defer 函数调用的时候其实会生成一个_defer结构,一个函数中可能有多次defer调用,所以会生成多个这样的_defer结构,这些_defer结构链式存储构成一个_defer链表,当前goroutine的_defer指向这个链表的头节点

_defer 的结构定义在src/src/runtime/runtime2.go中,源码如下:

// A _defer holds an entry on the list of deferred calls.// If you add a field here, add code to clear it in deferProcStack.// This struct must match the code in cmd/compile/internal/ssagen/ssa.go:deferstruct// and cmd/compile/internal/ssagen/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_deferstruct{// 标记位,标志当前defer结构是否是分配在堆上heapboolrangefuncbool// true for rangefunc list// 调用方的sp寄存器指针,即栈指针spuintptr// sp at time of defer// 调用方的程序计数器指针pcuintptr// pc at time of defer// defer注册的延迟执行的函数fnfunc()// can be nil for open-coded defers// defer链表link*_defer// next defer on G; can point to either heap or stack!// If rangefunc is true, *head is the head of the atomic linked list// during a range-over-func execution.head*atomic.Pointer[_defer]}

底层存储如下图:

defer函数在注册的时候,创建的_defer结构会依次插入到_defer链表的表头,在当前函数return的时候,依次从_defer链表的表头取出_defer结构执行里面的fn函数

头插法(push 到链表头),所以 defer 的执行顺序是后进先出 LIFO

defer的执行过程

在探究defer的执行过程之前,先简单看一下go语言程序的编译过程,go语言程序由.go文件编译成最终的二进制机器码主要有以下结果步骤

defer关键字的处理在生成SSA中间代码阶段,编译器遇到defer 语句的时候,会插入两种函数

  1. defer内存分配函数:deferproc(堆分配)或 deferprocStack(栈分配)

  2. 执行函数:deferreturn

下面分别看一下这两种函数的执行过程

defer的处理逻辑在cmd/compile/internal/ssagen/ssa.go文件中的state.stmt()方法中,由于源码过长,这里只贴部分重要代码:

caseir.ODEFER:// 如果节点defer节点n:=n.(*ir.GoDeferStmt)ifbase.Debug.Defer>0{vardefertypestringifs.hasOpenDefers{defertype="open-coded"// 开放编码}elseifn.Esc()==ir.EscNever{defertype="stack-allocated"// 栈分配}else{defertype="heap-allocated"// 堆分配}base.WarnfAt(n.Pos(),"%s defer",defertype)}ifs.hasOpenDefers{// 如果可以开放编码,即内联实现s.openDeferRecord(n.Call.(*ir.CallExpr))// 就使用开放编码这种方式}else{d:=callDefer// 否则先默认使用堆分配的模式ifn.Esc()==ir.EscNever{// 没有内存逃逸,使用栈分配的方式实现d=callDeferStack}s.callResult(n.Call.(*ir.CallExpr),d)}

从上述代码可以看出,defer的是现有三种实现方式,在栈上分配内存,在堆上分配内存以及使用开放编码的方式。
会优先使用内联方式,当内联不满足,且没有发生内存逃逸的情况下,使用栈分配的方式,这两种情况都不符合的情况下在使用堆分配,这样做的好处是提升性能。

_defer内存分配

defer 的实现方式由编译器决定:优先使用 open-coded defer;如果不能开放编码,则根据逃逸分析决定 _defer 记录是栈上分配还是堆上分配。是否堆分配取决于 defer 记录及其捕获的环境/参数是否会逃逸,而不是简单取决于被 defer 的函数“是否简单”或“函数内部是否动态分配”。

在上面的分析中我们可以看出在不同的情况下,_defer结构分配在不同的地方,可能分配在堆上也可能分配在栈上,这两种分配方式调用的函数是不同的,堆上分配实际调用的是 runtime.deferproc 函数,栈上分配内存调用的是 runtime.deferprocStack 函数,下面分别来看看这两个函数都做了些什么工作?

堆上分配

先看deferproc 函数,在堆上分配内存,go 1.13 之前只有这个函数,说明go 1.13 之前,_defer只能在堆上分配。

src/runtime/panic.go

// Create a new deferred function fn, which has no arguments and results.// The compiler turns a defer statement into a call to this.funcdeferproc(fnfunc()){// 获取goroutine,defer在哪个goroutine中执行gp:=getg()ifgp.m.curg!=gp{// go code on the system stack can't deferthrow("defer on system stack")}// 在堆中创建一个_defer对象d:=newdefer()// 将这个新建的defer对象加入到goroutine的defer链表头部d.link=gp._defer gp._defer=d d.fn=fn d.pc=sys.GetCallerPC()// We must not be preempted between calling GetCallerSP and// storing it to d.sp because GetCallerSP's result is a// uintptr stack pointer.d.sp=sys.GetCallerSP()}

重点看一下newdefer()这个函数

// Each P holds a pool for defers.// 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.funcnewdefer()*_defer{vard*_defer mp:=acquirem()// 获取逻辑处理器Ppp:=mp.p.ptr()// p的本地defer缓存池为空且全局defer缓存池不为空,从全局defer缓存池取出一个defer结构加入到p的本地defer缓存池iflen(pp.deferpool)==0&&sched.deferpool!=nil{lock(&sched.deferlock)forlen(pp.deferpool)<cap(pp.deferpool)/2&&sched.deferpool!=nil{d:=sched.deferpool sched.deferpool=d.link d.link=nilpp.deferpool=append(pp.deferpool,d)}unlock(&sched.deferlock)}// p的本地defer缓存池取出一个defer结构ifn:=len(pp.deferpool);n>0{d=pp.deferpool[n-1]pp.deferpool[n-1]=nilpp.deferpool=pp.deferpool[:n-1]}releasem(mp)mp,pp=nil,nil// p的本地defer缓存池和全局defer缓存池都没有可用的defer结构,在堆上创建一个ifd==nil{// Allocate new defer.d=new(_defer)}d.heap=truereturnd}

可以看出堆上defer的创建思想借助了内存复用,用到了内存池的思想,创建defer的过程是:优先在P的本地和全局的defer缓存池里找到一个可用的defer结构返回,找不到在去堆上创建

栈上分配

下面看一下 runtime.deferprocStack 函数,在栈上分配_defer,这个函数是go 1.13 之后引入的,优化defer性能的,显然在栈上分配的效率更高

runtime.deferprocStack 源码如下:

// deferprocStack queues a new deferred function with a defer record on the stack.// The defer record must have its fn field initialized.// All other fields can contain junk.// Nosplit because of the uninitialized pointer fields on the stack.////go:nosplit// 在调用这个函数之前,defer结构已经在栈上创建好,这里只是作为参数传进来赋值funcdeferprocStack(d*_defer){// 获取goroutine,defer在哪个goroutine中执行gp:=getg()ifgp.m.curg!=gp{// go code on the system stack can't deferthrow("defer on system stack")}// fn is already set.// The other fields are junk on entry to deferprocStack and// are initialized here.// 堆上分配为falsed.heap=falsed.rangefunc=falsed.sp=sys.GetCallerSP()d.pc=sys.GetCallerPC()// The lines below implement:// d.panic = nil// d.fd = nil// d.link = gp._defer// d.head = nil// gp._defer = d// But without write barriers. The first three are writes to// the stack so they don't need a write barrier, and furthermore// are to uninitialized memory, so they must not use a write barrier.// The fourth write does not require a write barrier because we// explicitly mark all the defer structures, so we don't need to// keep track of pointers to them with a write barrier.*(*uintptr)(unsafe.Pointer(&d.link))=uintptr(unsafe.Pointer(gp._defer))*(*uintptr)(unsafe.Pointer(&d.head))=0*(*uintptr)(unsafe.Pointer(&gp._defer))=uintptr(unsafe.Pointer(d))}

Go 在编译的时候在 SSA中间代码阶段,如果判断出_defer需要在栈上分配,则编译器会直接在函数调用栈上初始化 _defer 记录,并作为参数传递给 deferprocStack 函数。

开放编码

再看一下defer的第三种实现方式,开放编码。这种方式是在go1.14 引入的继续优化defer实现性能的方式。在go1.14 中通过代码内联优化,使得函数末尾直接对 defer 函数进行调用,减少了函数调用开销。其主要逻辑位于 cmd/compile/internal/walk/stmt.go文件的 walkStmt()函数和 cmd/compile/internal/ssagen/ssa.go 的 buildssa()函数,函数较长,这里看一下关键代码。

walkStmt()函数:

caseir.ODEFER:// 如果节点defer节点n:=n.(*ir.GoDeferStmt)ir.CurFunc.SetHasDefer(true)ir.CurFunc.NumDefers++ifir.CurFunc.NumDefers>maxOpenDefers{// maxOpenDefers = 8// defer函数的个数多余8个时,不能用开放编码模式ir.CurFunc.SetOpenCodedDeferDisallowed(true)}ifn.Esc()!=ir.EscNever{// If n.Esc is not EscNever, then this defer occurs in a loop,// so open-coded defers cannot be used in this function.ir.CurFunc.SetOpenCodedDeferDisallowed(true)}fallthrough

这里分析一下 n.Esc() != ir.EscNever 这个条件:
通过源码注释可以看到,这里其实就是判断defer是否在循环体内,因为 defer 在 for 循环中调用,编译器不确定会执行多少次,会逃逸到堆上,这样defer就只能分配在堆中了。所以在使用defer 延迟调用的时候,尽量不要在循环中使用,否则可能导致性能问题。

buildssa()函数:

// build时候的没有设置-N,允许内联s.hasOpenDefers=base.Flag.N==0&&s.hasdefer&&!s.curfn.OpenCodedDeferDisallowed()switch{casebase.Debug.NoOpenDefer!=0:s.hasOpenDefers=falsecases.hasOpenDefers&&(base.Ctxt.Flag_shared||base.Ctxt.Flag_dynlink)&&base.Ctxt.Arch.Name=="386":// Don't support open-coded defers for 386 ONLY when using shared// libraries, because there is extra code (added by rewriteToUseGot())// preceding the deferreturn/ret code that we don't track correctly.//// TODO this restriction can be removed given adjusted offset in computeDeferReturn in cmd/link/internal/ld/pcln.gos.hasOpenDefers=false}ifs.hasOpenDefers&&s.instrumentEnterExit{// Skip doing open defers if we need to instrument function// returns for the race detector, since we will not generate that// code in the case of the extra deferreturn/ret segment.s.hasOpenDefers=false}ifs.hasOpenDefers{// Similarly, skip if there are any heap-allocated result// parameters that need to be copied back to their stack slots.for_,f:=ranges.curfn.Type().Results(){if!f.Nname.(*ir.Name).OnStack(){s.hasOpenDefers=falsebreak}}}// defer所在函数返回值个数和defer函数个数乘积不能大于15ifs.hasOpenDefers&&s.curfn.NumReturns*s.curfn.NumDefers>15{// Since we are generating defer calls at every exit for// open-coded defers, skip doing open-coded defers if there are// too many returns (especially if there are multiple defers).// Open-coded defers are most important for improving performance// for smaller functions (which don't have many returns).s.hasOpenDefers=false}

总结一下:在go1.14之后,go会优先采用内联的方式处理defer函数调用,但是需要满足以下几个条件:
• build编译的时候没有设置 -N
• defer 函数个数没有超过 8 个
• defer所在函数返回值个数和defer函数个数乘积不超过15
• defer没有出现在循环语句中

defer函数执行

在给defer分配好内存之后,剩下的就是执行了。在函数退出的时候,deferreturn 来执行defer链表上的各个defer函数。函数源码如下:

// deferreturn runs deferred functions for the caller's frame.// The compiler inserts a call to this at the end of any// function which calls defer.funcdeferreturn(){varp _panic p.deferreturn=true// 遍历goroutine的defer链表p.start(sys.GetCallerPC(),unsafe.Pointer(sys.GetCallerSP()))for{fn,ok:=p.nextDefer()if!ok{break}// 执行函数调用fn()}}// start initializes a panic to start unwinding the stack.//// If p.goexit is true, then start may return multiple times.func(p*_panic)start(pcuintptr,sp unsafe.Pointer){gp:=getg()// Record the caller's PC and SP, so recovery can identify panics// that have been recovered. Also, so that if p is from Goexit, we// can restart its defer processing loop if a recovered panic tries// to jump past it.p.startPC=sys.GetCallerPC()p.startSP=unsafe.Pointer(sys.GetCallerSP())ifp.deferreturn{// 获取调用栈的栈顶指针p.sp=sp// 开放编码模式 内联处理ifs:=(*savedOpenDeferState)(gp.param);s!=nil{// recovery saved some state for us, so that we can resume// calling open-coded defers without unwinding the stack.gp.param=nilp.retpc=s.retpc p.deferBitsPtr=(*byte)(add(sp,s.deferBitsOffset))p.slotsPtr=add(sp,s.slotsOffset)}return}p.link=gp._panic gp._panic=(*_panic)(noescape(unsafe.Pointer(p)))// Initialize state machine, and find the first frame with a defer.//// Note: We could use startPC and startSP here, but callers will// never have defer statements themselves. By starting at their// caller instead, we avoid needing to unwind through an extra// frame. It also somewhat simplifies the terminating condition for// deferreturn.p.lr,p.fp=pc,sp p.nextFrame()}// nextDefer returns the next deferred function to invoke, if any.//// Note: The "ok bool" result is necessary to correctly handle when// the deferred function itself was nil (e.g., "defer (func())(nil)").func(p*_panic)nextDefer()(func(),bool){gp:=getg()if!p.deferreturn{ifgp._panic!=p{throw("bad panic stack")}ifp.recovered{mcall(recovery)// does not returnthrow("recovery failed")}}// The assembler adjusts p.argp in wrapper functions that shouldn't// be visible to recover(), so we need to restore it each iteration.p.argp=add(p.startSP,sys.MinFrameSize)for{forp.deferBitsPtr!=nil{bits:=*p.deferBitsPtr// Check whether any open-coded defers are still pending.//// Note: We need to check this upfront (rather than after// clearing the top bit) because it's possible that Goexit// invokes a deferred call, and there were still more pending// open-coded defers in the frame; but then the deferred call// panic and invoked the remaining defers in the frame, before// recovering and restarting the Goexit loop.ifbits==0{p.deferBitsPtr=nilbreak}// Find index of top bit set.i:=7-uintptr(sys.LeadingZeros8(bits))// Clear bit and store it back.bits&^=1<<i*p.deferBitsPtr=bitsreturn*(*func())(add(p.slotsPtr,i*goarch.PtrSize)),true}Recheck:ifd:=gp._defer;d!=nil&&d.sp==uintptr(p.sp){ifd.rangefunc{deferconvert(d)popDefer(gp)gotoRecheck}// 非内联模式// 获取defer的执行函数fn:=d.fn p.retpc=d.pc// Unlink and free.popDefer(gp)returnfn,true}if!p.nextFrame(){returnnil,false}}}// popDefer pops the head of gp's defer list and frees it.funcpopDefer(gp*g){d:=gp._defer// defer上的函数指针置空d.fn=nil// Can in theory point to the stack// We must not copy the stack between the updating gp._defer and setting// d.link to nil. Between these two steps, d is not on any defer list, so// stack copying won't adjust stack pointers in it (namely, d.link). Hence,// if we were to copy the stack, d could then contain a stale pointer.// 遍历下一个defer结构gp._defer=d.link d.link=nil// After this point we can copy the stack.if!d.heap{return}mp:=acquirem()pp:=mp.p.ptr()iflen(pp.deferpool)==cap(pp.deferpool){// Transfer half of local cache to the central cache.varfirst,last*_deferforlen(pp.deferpool)>cap(pp.deferpool)/2{n:=len(pp.deferpool)d:=pp.deferpool[n-1]pp.deferpool[n-1]=nilpp.deferpool=pp.deferpool[:n-1]iffirst==nil{first=d}else{last.link=d}last=d}lock(&sched.deferlock)last.link=sched.deferpool sched.deferpool=firstunlock(&sched.deferlock)}*d=_defer{}// 释放defer结构,优先归还到defer缓冲池中pp.deferpool=append(pp.deferpool,d)releasem(mp)mp,pp=nil,nil}

当 go函数的 return 关键字执行的时候,触发 call 调用 deferreturn 函数,deferreturn函数的执行逻辑也很简单,就是遍历goroutine上的defer链表,从表头开始遍历,依次取出defer结构执行defer结构中的函数执行。

总结:

  1. 遇到defer关键字,编译器会在编译阶段注册defer函数的时候插入 deferproc() 函数或者 deferprocStack 函数,在return之前插入deferreturn()函数

  2. defer函数的执行顺序是LIFO的,因为每次创建的defer结构都是插入到goroutine的defer链表表头

  3. defer结构的有三种实现方式,堆上分配,栈上分配还有内联实现

非内联模式defer结构上的fn字段就是对应的defer执行函数 ​ 而内联模式直接将defer函数展开在函数里面 ​ 你可以看到非内联模式调用的是defer结构里面的fn函数 ​ 而内联模式是直接运行的栈帧某段代码(defer函数展开的内容)

defer面试与分析

1、defer的底层数据结构是怎样的​

回顾这个图:

每个 defer 语句都对应一个_defer 实例,多个实例使用指针连接起来形成一个单链表,保存在 goroutine 数据结构中,每次插入_defer 实例,均插入到链表的头部,函数结束再一次从头部取出,从而形成后进先出的效果

2、循环体中能用defer调用吗? 会有什么问题,为什么?

循环体中不要使用defer调用语句,一方面是会影响性能,另一方面是可能会发生一些意想不到的结果

首先,在循环中使用defer可能会发生内存逃逸(逃逸就是“编译器不敢放栈上,只能放堆上”,代价通常是更多分配和 GC,性能会变差。),这样defer有可能分配到堆上,相比于栈上分配和内联方式,是性能最差的一种内存分配方式,会导致程序的性能问题

另外,可能会带来一些系统问题。比如在一个循环中,用defer函数来操作文件,如下:

for_,filename:=rangefilenames{f,err:=os.Open(filename)iferr!=nil{returnerr}deferf.Close()}

这段代码很可能会用尽所有文件描述符。因为 defer 语句不到函数的最后一刻是不会执行的,也就是说文件始终得不到关闭

3、defer能修改返回值吗,defer与return的先后关系是怎样的?

defer可以修改返回值,当函数的返回值是非匿名的,有显示返回值的时候,defer可以修改返回值。函数的返回其实不是一个原子操作,可以理解为三个步骤:

  1. 设置返回值

  2. 执行defer语句

  3. 将结果返回

4、多个defer的执行顺序是怎样的?

后进先出,类似于栈,先调用的defer语句后执行

之后我会持续更新,如果喜欢我的文章,请记得一键三连哦,点赞关注收藏,你的每一个赞每一份关注每一次收藏都将是我前进路上的无限动力 !!!↖(▔▽▔)↗感谢支持!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询