defer语句用于延迟函数的调用.常用于关闭文件描述符 释放锁等资源释放场
景.defer语句采用后进先出的设计.类
似于栈的方式.函数执行时.每遇到一个defer都会把一个函数压入栈中.函数返回前再
将函数从栈中取出执行.最早被压入栈中的函数最晚执行.
1.defer用法:
defer关键字后接一个匿名函数:
defer func() {
fmt.Println("hello world")
}()
defer关键字后接一个函数调用:
file, err := os.Open("name")
if err != nil {
fmt.Printf("open file error:%v\n", err)
}
defer file.Close()
2.使用场景:
1).释放资源.defer常用于关闭文件句柄 数据库连接 停止定时器 Ticker及关闭通道
等资源清理场景.
2).流程控制.defer也常用于控制函数的顺序执行.比如配合wait.Group实现等待协
程退出.
3).异常处理.defer也常用于处理异常.与recover配合可以消除panic.另外recover
只能用于defer函数.
3.3行为规则:
1).延迟函数的参数在defer语句出现时就已经确定了.
官方示例:
func main() {
i := 0
defer fmt.Println(i)
i++
}
defer语句中的fmt.Println()参数i值在defer出现时就已经确定了.实际上是复制了
一份.后面对变量i的修改也不会影响fmt.Println()函数的执行.仍然打印0.
执行结果:
2).延迟函数按后进先出(LIFO)的顺序执行.即先出现的defer最后执行.
定义defer类似于入栈操作.执行defer类似于出栈操作.
设计defer的初衷是简化函数返回时资源清理的动作.资源往往有顺序依赖.比如先申
请A资源.再根据A资源申请B资源.根据B资源申请C资源.申请顺序是A->B->C.释放
时往往又要反向进行.这就是把defer设计成LIFO的原因.
3).延迟函数可能操作主函数的具名返回值.
定义defer的函数(下称主函数)可能有返回值.返回值可能有名字(具名返回值).也可能
没有名字(匿名返回值).延迟函数可能会影响返回值.
1.函数返回过程:
有一个事实必须了解.关键字return不是一个原子操作.实际上return只代表汇编指令
ret.即跳转程序执行,比如语句return i.实际上是分两步执行.即先将i值存入栈中作为
返回值.然后执行跳转.而defer的执行时机正是在跳转前.所以说defer执行时还是有
机会操作返回值的.
示例:
func main() {
fmt.Println(deferTest())
}
func deferTest() (result int) {
i := 1
defer func() {
result++
}()
return i
}
执行结果:
return语句拆分如下:
func deferTest() (result int) {
i := 1
defer func() {
result++
}()
result = i
return i
}
加入defer拆分如下:
func deferTest() (result int) {
i := 1
defer func() {
result++
}()
result = i
result++
return i
}
2.主函数拥有匿名返回值.返回字面值:
一个主函数拥有一个匿名返回值.返回时使用字面值.比如返回1 2 这样的值.
这种情况下defer语句无法操作返回值.
示例:
func foo() int {
var i int
defer func() {
i++
}()
return 1
}
上面的return语句直接把1写入栈中作为返回值.延迟函数无法操作该返回
值.所以就无法影响返回值.
3.主函数拥有匿名返回值.返回变量.
一个主函数拥有一个匿名返回值.返回本地或全局变量.这种defer语句可以
引用返回值.但不会改变返回值.
示例:
func main() {
fmt.Println(foo())
}
func foo() int {
var i int
defer func() {
i++
}()
return i
}
执行结果:
上面的函数返回一个局部变量.同事defer函数也会操作这个局部变量.对于匿名返回
值来说.可以假定仍然有一个变量存储返回值.假定返回值为result.上面的返回语句可
以拆分为如下.
func foo() int {
var i int
defer func() {
i++
}()
result=i
i++
return
}
由于i是整型值.会将值赋值给result.所以在defer语句中修改i值.不会对函数返回值
造成影响.(上面代码只是展示理解.并不能执行)
4.主函数拥有具名返回值:
主函数声明语句带名字的返回值会被初始化为一个局部变量.函数内部可以像使用局
部变量一样使用该返回值.如果defer语句操作该返回值.则可能改变返回结果.
示例:
func main() {
fmt.Println(foo())
}
func foo() (ret int) {
defer func() {
ret++
}()
return 0
}
执行结果:
函数拆解如下:
func foo() (ret int) {
defer func() {
ret++
}()
ret = 0
ret++
return
}
函数真正返回前.在defer中对返回值做了+1操作.所以函数最终返回1.
3.实现原理:
数据结构:
源码位置:src/runtime/runtime2.go
type _defer struct {
heap bool
rangefunc bool // true for rangefunc list
sp uintptr // sp at time of defer
pc uintptr // pc at time of defer
fn func() // can be nil for open-coded defers
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]
}
结构字段:
heap:标记defer实例是否分配在堆上. true分配在堆上(栈空间不足、defer 被闭
包捕获、跨 goroutine 传递时触发). false:分配在当前 goroutine 的栈上(默
认,性能最优).
rangefunc: 标记是否为 range-over-func 场景的 defer
sp: 栈指针(stack pointer).defer 语句执行时的栈指针. 恢复 defer 执行时的栈
上下文,确保延迟函数能正确访问调用时的局部变量 .
pc: 程序计数器 .
fn: 延迟执行的函数 .
link: defer 链表指针.指向下一个 defer 实例.
head: range-over-func 场景的原子链表头.只有当rangefunc为true时生效.
从数据结构可以看出.每个defer实例都是一个函数的封装.它拥有执行函数必要的信
息.实际上编译器会把每个延迟函数编译成一个defer实例暂存到goroutine数据结
构中.待函数结束时在逐个取出执行.
每个defer语句对应一个_defer实例.多个实例通过link链接起来形成一个单链表.保
存到goroutine中.
源码位置:src/runtime/runtime2.go
type g struct {
...
_defer *_defer // innermost defer
...
}
每次插入实例时均插入_defer链表头部.函数执行结束再依次从头部取出.从而实现后进先出的效果.
创建和执行:
源码位置:src/runtime/panic.go
deferproc():
用于defer函数处理成_defer实例.并存入goroutine链表中.
func deferproc(fn func()) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
d := newdefer()
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()
// 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.
}
deferreturn():
用于将defer从goroutine链表中取出并执行.
func deferreturn() {
var p _panic
p.deferreturn = true
p.start(sys.GetCallerPC(), unsafe.Pointer(sys.GetCallerSP()))
for {
fn, ok := p.nextDefer()
if !ok {
break
}
fn()
}
}
4.性能优化:
1).堆defer:
编译器将defer语句编译成一个deferproc()函数调用.然后运行时执行deferproc函
数.deferproc函数会根据defer语句生产一个_defer(运行时内部数据结构名)实例
并插入goroutine的_defer链表头部.同时编译还会在函数尾部插入deferrturn函
数.deferreturn函数会逐个取出_defer实例并执行.
堆defer的特定是新创建的defer节点存储在堆中.deferproc函数会将被延迟的函数
组成一个_defer实例并复制到_defer节点中.deferreturn函数消费完_defer后.再
将节点销毁.
堆defer节点的痛点主要在于频繁的堆内存分配及释放.性能稍差.
2).栈defer:
栈defer正是为了提高堆defer内存使用效率而引入的.编译器将尽量将defer语句编
译成一个deferprocStack()函数调用.deferprocStack()的工作机制与defer
proc()类似.区别在于编译器会直接在栈上预留_defer的存储空
间.deferprocStack()不在需要再分配空间.deferprocStack()仍然需要将_defer
插入协程g的_defer链表中.
源码如下:
位置:src/runtime/panic.go
func deferprocStack(d *_defer) {
gp := getg()
if gp.m.curg != gp {
// go code on the system stack can't defer
throw("defer on system stack")
}
d.heap = false
d.rangefunc = false
d.sp = sys.GetCallerSP()
d.pc = sys.GetCallerPC()
// 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))
return0()
// No code can go here - the C return register has
// been set and must not be clobbered.
}
可以看到d.heap=false.
在函数结尾处.编译器仍然会插入deferreturn()函数.该函数执行过程中与堆defer
类似.不同的是.执行结束后不需要释放内存了.
编译器会尽可能的把defer语句编译成栈类型.但由于栈空间也有限.并不能把所有的
defer都存储在栈中.所以还需要保留堆.
5.开放编码defer:
无论堆defer还是栈defer.编译器只能把defer替换成相应的函数.对于堆defer而言
是deferproc()函数.对于栈defer而言则是deferprocStack()函数.只能由运行时调
用这些函数.然后创建_defer节点并存储.最后通过deferreturn()函数取出这些
_defer节点在执行.
如果编译器能把defer语句直接翻译成相应的执行代码并插入函数尾部.那么就会节省
_defer节点转储的代价.事实上.开放编码defer采用的就是这种机制.
无法进行开放编码优化:
1).禁用编译器优化
由于编码器在编译时决定了defer的类型.对于开放编码而言还需要在用户的函数体中
插入执行代码.属于编译优化的一种.所以如果编译项目时禁用了编译优化.那么编译器
不会把defer处理成开放编码类型.
2).循环语句:
如果defer语句出现在循环中.那么编译器在编译阶段无法确定最终生成了多少个de
fer.也就无法在函数尾部插入代码.所以不能把defeer处理成开放编码.
3).限制defer数量:
在编译器将defer语句编译成开放编码defer时.单个函数内defer语句越多.编译器实
现越复杂.支持无限的defer语句是不现实的.最终必须在设计复杂度及实际场景之间
做权衡和取舍.
示例:
func foo() {
//运行时才能决定能否满足条件.
if flag {
defer fmt.Println("hello go")
}
}
上面的例子.虽然无法在编译阶段确定判断条件是否成立.但还是会把上面的
defer语句插入函数尾部.复杂一点的是运行时需要有手段来根据条件判断
成立与否是否要执行编译插入的代码.
因此Go引入了deferBit.即编译器在函数内创建一个字节(8bit)的变量.并
在函数插入相应代码.确保运行时每经过一个defer语句(说明defer语句被
触发).就把相应位置置为1.在函数尾部.运行时就可以根据deferBits相应位
的值来决定是否要执行插入代码.
因为deferBit长度为一个字节.即8bit.这就是为什么单个函数的defer数量
被限制在8以下的原因.
在编译器中.如果单个函数defer数量超过8.则直接对该函数禁用开放编码.
如果函数中return语句的个数和defer语句的个数乘积超过了15.也直接禁
用开发编码.