Go进阶之defer

21 阅读10分钟

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.也直接禁

用开发编码.