Golang原理剖析(defer、defer面试与分析)

文章目录

defer是什么​

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

defer的使用形式

go 复制代码
defer func(args)

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

defer的底层结构

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

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

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 _defer struct {
	// 标记位,标志当前defer结构是否是分配在堆上
	heap      bool
	rangefunc bool    // true for rangefunc list

	// 调用方的sp寄存器指针,即栈指针
	sp        uintptr // sp at time of defer

	// 调用方的程序计数器指针
	pc        uintptr // pc at time of defer

	// defer注册的延迟执行的函数
	fn        func()  // 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()方法中,由于源码过长,这里只贴部分重要代码:

go 复制代码
case ir.ODEFER:     // 如果节点defer节点
    n := n.(*ir.GoDeferStmt)
    if base.Debug.Defer > 0 {
        var defertype string
        if s.hasOpenDefers {
            defertype = "open-coded"      // 开放编码
        } else if n.Esc() == ir.EscNever {
            defertype = "stack-allocated" // 栈分配
        } else {
            defertype = "heap-allocated"  // 堆分配
        }
        base.WarnfAt(n.Pos(), "%s defer", defertype)
    }
    if s.hasOpenDefers {   // 如果可以开放编码,即内联实现
        s.openDeferRecord(n.Call.(*ir.CallExpr))  // 就使用开放编码这种方式
    } else {
        d := callDefer      // 否则先默认使用堆分配的模式
        if n.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

go 复制代码
// Create a new deferred function fn, which has no arguments and results.
// The compiler turns a defer statement into a call to this.
func deferproc(fn func()) {
	// 获取goroutine,defer在哪个goroutine中执行
	gp := getg()
	if gp.m.curg != gp {
		// go code on the system stack can't defer
		throw("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()这个函数

go 复制代码
// 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.
func newdefer() *_defer {
	var d *_defer
	mp := acquirem()
	// 获取逻辑处理器P
	pp := mp.p.ptr()
	// p的本地defer缓存池为空且全局defer缓存池不为空,从全局defer缓存池取出一个defer结构加入到p的本地defer缓存池
	if len(pp.deferpool) == 0 && sched.deferpool != nil {
		lock(&sched.deferlock)
		for len(pp.deferpool) < cap(pp.deferpool)/2 && sched.deferpool != nil {
			d := sched.deferpool
			sched.deferpool = d.link
			d.link = nil
			pp.deferpool = append(pp.deferpool, d)
		}
		unlock(&sched.deferlock)
	}
	// p的本地defer缓存池取出一个defer结构
	if n := len(pp.deferpool); n > 0 {
		d = pp.deferpool[n-1]
		pp.deferpool[n-1] = nil
		pp.deferpool = pp.deferpool[:n-1]
	}
	releasem(mp)
	mp, pp = nil, nil

	// p的本地defer缓存池和全局defer缓存池都没有可用的defer结构,在堆上创建一个
	if d == nil {
		// Allocate new defer.
		d = new(_defer)
	}
	d.heap = true
	return d
}

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

栈上分配

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

runtime.deferprocStack 源码如下:

go 复制代码
// 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结构已经在栈上创建好,这里只是作为参数传进来赋值
func deferprocStack(d *_defer) {
	// 获取goroutine,defer在哪个goroutine中执行
	gp := getg()
	if gp.m.curg != gp {
		// go code on the system stack can't defer
		throw("defer on system stack")
	}

	// fn is already set.
	// The other fields are junk on entry to deferprocStack and
	// are initialized here.
	// 堆上分配为false
	d.heap = false
	d.rangefunc = false
	d.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()函数:

go 复制代码
case ir.ODEFER:     // 如果节点defer节点
    n := n.(*ir.GoDeferStmt)
    ir.CurFunc.SetHasDefer(true)
    ir.CurFunc.NumDefers++
    if ir.CurFunc.NumDefers > maxOpenDefers {   // maxOpenDefers = 8
        // defer函数的个数多余8个时,不能用开放编码模式
        ir.CurFunc.SetOpenCodedDeferDisallowed(true)
    }

    if n.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()函数:

go 复制代码
	// build时候的没有设置-N,允许内联
	s.hasOpenDefers = base.Flag.N == 0 && s.hasdefer && !s.curfn.OpenCodedDeferDisallowed()
	switch {
	case base.Debug.NoOpenDefer != 0:
		s.hasOpenDefers = false
	case s.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.go
		s.hasOpenDefers = false
	}
	if s.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
	}
	if s.hasOpenDefers {
		// Similarly, skip if there are any heap-allocated result
		// parameters that need to be copied back to their stack slots.
		for _, f := range s.curfn.Type().Results() {
			if !f.Nname.(*ir.Name).OnStack() {
				s.hasOpenDefers = false
				break
			}
		}
	}
	// defer所在函数返回值个数和defer函数个数乘积不能大于15
	if s.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函数。函数源码如下:

go 复制代码
// 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.
func deferreturn() {
	var p _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(pc uintptr, 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())

	if p.deferreturn {
		// 获取调用栈的栈顶指针
		p.sp = sp
	
		// 开放编码模式 内联处理
		if s := (*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 = nil

			p.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 {
		if gp._panic != p {
			throw("bad panic stack")
		}

		if p.recovered {
			mcall(recovery) // does not return
			throw("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 {
		for p.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.
			if bits == 0 {
				p.deferBitsPtr = nil
				break
			}

			// Find index of top bit set.
			i := 7 - uintptr(sys.LeadingZeros8(bits))

			// Clear bit and store it back.
			bits &^= 1 << i
			*p.deferBitsPtr = bits

			return *(*func())(add(p.slotsPtr, i*goarch.PtrSize)), true
		}

	Recheck:
		if d := gp._defer; d != nil && d.sp == uintptr(p.sp) {
			if d.rangefunc {
				deferconvert(d)
				popDefer(gp)
				goto Recheck
			}
			
			// 非内联模式
			// 获取defer的执行函数
			fn := d.fn

			p.retpc = d.pc

			// Unlink and free.
			popDefer(gp)

			return fn, true
		}

		if !p.nextFrame() {
			return nil, false
		}
	}
}

// popDefer pops the head of gp's defer list and frees it.
func popDefer(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()
	if len(pp.deferpool) == cap(pp.deferpool) {
		// Transfer half of local cache to the central cache.
		var first, last *_defer
		for len(pp.deferpool) > cap(pp.deferpool)/2 {
			n := len(pp.deferpool)
			d := pp.deferpool[n-1]
			pp.deferpool[n-1] = nil
			pp.deferpool = pp.deferpool[:n-1]
			if first == nil {
				first = d
			} else {
				last.link = d
			}
			last = d
		}
		lock(&sched.deferlock)
		last.link = sched.deferpool
		sched.deferpool = first
		unlock(&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函数来操作文件,如下:

go 复制代码
for _, filename := range filenames {
    f, err := os.Open(filename)
    if err != nil {
        return err
    }

    defer f.Close()
}

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

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

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

  1. 设置返回值

  2. 执行defer语句

  3. 将结果返回

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

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

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

相关推荐
越甲八千1 小时前
FastAPI传参类型
开发语言·python·fastapi
南山乐只1 小时前
Java并发原生工具:原子类 (Atomic Classes)
java·开发语言·后端
Hanson851 小时前
几何建模-低代码可视化建模技术
经验分享·开源·cad平台
一颗青果1 小时前
C++下的atomic | atmoic_flag | 内存顺序
java·开发语言·c++
Sylvia-girl2 小时前
Java之异常
java·开发语言
郝学胜-神的一滴2 小时前
Python对象的自省机制:深入探索对象的内心世界
开发语言·python·程序人生·算法
说私域2 小时前
全民电商时代下的链动2+1模式与S2B2C商城小程序:社交裂变与供应链协同的营销革命
开发语言·人工智能·小程序·php·流量运营
爱丽_2 小时前
Spring 框架
java·后端·spring
乾复道2 小时前
巧用终端,每天节省2小时
汇编·经验分享·vim