golang defer关键词执行原理与代码分析

155 阅读5分钟

使用的go版本为 go1.21.2

首先我们写一个简单的defer调度代码

package main

import "fmt"

func main() {
	defer func() {
		fmt.Println("xiaochuan")
	}()
}

通过go build -gcflags -S main.go获取到对应的汇编代码

汇编截图.png

可以在图中看到有个CALL runtime.deferreturn(SB) 调度这个是编译器插入的defer执行调度

我们先来看一下defer构造体的底层源码

defer结构体

//代码在GOROOT/src/runtime/runtime2.go中

type _defer struct {
    started   bool        // 表示是否已经开始执行
    heap      bool        // 标志是否分配在堆上
    openDefer bool        // 标志是否对应于一个带有 open-coded defers 的栈帧
    sp        uintptr     // 执行时的栈指针
    pc        uintptr     // 执行时的程序计数器
    fn        func()      // 存储被延迟执行的函数
    _panic    *_panic     // 当前执行的 panic(如果有的话)
    link      *_defer     // 在G(goroutine)上指向下一个延迟结构,可以指向堆或栈

    // 如果 openDefer 为 true,则以下字段记录与具有 open-coded defers 的栈帧相关的值。
    // 在这种情况下,sp 字段上面的 sp 是栈帧的 sp,而 pc 是关联函数中 deferreturn 调用的地址。
    fd       unsafe.Pointer // 与栈帧相关的函数的 funcdata
    varp     uintptr        // 栈帧的 varp 值
    framepc  uintptr        // 栈帧关联的当前 pc
}

deferreturn源码与解读

//代码在GOROOT/src/runtime/panic.go中
func deferreturn() {
    gp := getg() //获取当前运行G

    for {
        //逐步获取当前G中的defer调用
        d := gp._defer

        // 如果获取到的构造体为空,直接返回。
        if d == nil {
            return
        }

        // 获取调用 defer 语句的函数的栈指针。
        sp := getcallersp()

        // 如果_defer里面存的栈指针与当前函数的栈指针不匹配,直接返回。
        // 说明数据存在改写不给予处理
        if d.sp != sp {
            return
        }

        // 如果_defer使用了 open-coded defers(编码的延迟调用)
        if d.openDefer {
            // 运行 open-coded defers 的帧。
            done := runOpenDeferFrame(d)

            // 如果 open-coded defers 没有完成,抛出异常。
            if !done {
                throw("unfinished open-coded defers in deferreturn")
            }

            // 将_defer从G的延迟链表移除,释放对应的_defer构造体资源
            gp._defer = d.link
            freedefer(d)
            return
        }

        // 获取_defer中保存的执行函数
        fn := d.fn
        d.fn = nil

        // 从G中移除当前_defer,释放其资源。
        gp._defer = d.link
        freedefer(d)

        // 执行延迟函数。
        fn()
    }
}

freedefer源码与解读

//代码在GOROOT/src/runtime/panic.go中
func freedefer(d *_defer) {
    // _defer 结构的 link 字段设置为 nil
    d.link = nil

    // 如果还存在_panic字段,调用 freedeferpanic 函数
    if d._panic != nil {
        freedeferpanic()
    }

    // 如果调度函数不为 nil,调用 freedeferfn 函数
    if d.fn != nil {
        freedeferfn()
    }

    // 如果不在堆上,直接返回
    if !d.heap {
        return
    }
    // 通过当前G的m字段去拿到对应的M
    mp := acquirem()
    // 获取与M绑定的P
    pp := mp.p.ptr()

    // 如果P中的本地缓存已满
    // 将一半的defer池放入到调度器中去
    // 调度器相当于全局池,具体使用是有锁,所以优先使用本地池
    if len(pp.deferpool) == cap(pp.deferpool) {
        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
        }
        // 获取调度器中的defer锁
        lock(&sched.deferlock)
        //放入到全局池
        last.link = sched.deferpool
        sched.deferpool = first
        //释放调度器中的defer锁
        unlock(&sched.deferlock)
    }

    // 将 _defer 结构清零
    *d = _defer{}

    // 将 _defer 结构放回P的本地缓存
    pp.deferpool = append(pp.deferpool, d)

    // 释放 M
    releasem(mp)
    mp, pp = nil, nil
}

看老的版本的一些文章介绍在使用 defer func(){}() 时编译器会将转换为runtime.deferproc Go新版本没看到汇编对应的调度过程,希望有大哥能帮忙解答一下新版本是如何调度到runtime.deferproc函数
(2023/11/23补充)go1.14开始不再调用deferproc函数具体参考资料 (open-coded defer)

deferproc源码与解读

//代码在GOROOT/src/runtime/panic.go中
func deferproc(fn func()) {
    // 获取当前G
    gp := getg()

    // 检查G是否在系统栈上
    if gp.m.curg != gp {
        // 系统栈上的 Go 代码不能使用 defer
        throw("defer on system stack")
    }

    // 创建一个新的 defer 结构
    d := newdefer()

    // 检查新创建的 defer 结构的 _panic 字段是否为 nil
    if d._panic != nil {
        throw("deferproc: d.panic != nil after newdefer")
    }

    // 将新的defer结构添加到当前G的defer链表中
    d.link = gp._defer
    gp._defer = d

    // 设置defer触发函数
    d.fn = fn
    //GOROOT/src/runtime/stubs.go
    //注释是这么说的返回其调用者的调用者的程序计数器
    //具体实现在汇编层
    d.pc = getcallerpc()

    //GOROOT/src/runtime/stubs.go
    //注释是这么说的返回其调用者的调用者的堆栈指针
    //具体实现在汇编层
    d.sp = getcallersp()

    //GOROOT/src/runtime/stubs.go
    //return0 是一个用于从 deferproc 返回 0 的存根。
    //它在 deferproc 的最后调用来发出信号
    //调用 Go 函数时不应跳转
    //推迟返回。
    //具体实现在汇编层
    return0()

    // 不能在这里放置代码 - C 返回寄存器已设置,不能被破坏。
}

newdefer源码与解读

//代码在GOROOT/src/runtime/panic.go中
func newdefer() *_defer {
    // 声明一个_defer指针变量
    var d *_defer

    // 通过当前G的m字段去拿到对应的M
    mp := acquirem()

    // 获取与M绑定的P
    pp := mp.p.ptr()

    // 检查P中 deferpool 是否为空,且调度器中有可用的 defer 结构体
    if len(pp.deferpool) == 0 && sched.deferpool != nil {
        // 获取调度器中的defer锁
        lock(&sched.deferlock)

        // 将调度器中的deferpool转移到P的本地池中去
        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)
        }

        // 释放调度器中的defer锁
        unlock(&sched.deferlock)
    }

    // 检查P的本地池中是否有可用的defer结构体
    if n := len(pp.deferpool); n > 0 {
        // 从本地池拿出来一个 defer 结构体
        d = pp.deferpool[n-1]
        pp.deferpool[n-1] = nil
        pp.deferpool = pp.deferpool[:n-1]
    }

    // 释放 M
    releasem(mp)
    mp, pp = nil, nil

    // 如果没有找到可用的 defer 结构体,则分配一个新的
    if d == nil {
        d = new(_defer)
    }

    // 将 'heap' 字段设置为 true 并返回 defer 结构体
    d.heap = true
    return d
}

总结

从上面的源码我们可以了解到defer的大致逻辑,当使用defer关键词时,会将当前要延迟的函数加入到G的延迟链表中去,当我们的函数执行完成后会触发deferreturn调度将G中的延迟链表循环执行一遍,来达到延迟执行的目的