golang中defer的实现原理

发布时间:2021-09-09 15:36:46 作者:柒染
来源:亿速云 阅读:151

golang中defer的实现原理,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。

defer是golang提供的关键字,在函数或者方法执行完成,返回之前调用。
每次defer都会将defer函数压入栈中,调用函数或者方法结束时,从栈中取出执行,所以多个defer的执行顺序是先入后出。

for i := 0; i <= 3; i++ {
    defer fmt.Print(i)
}
//输出结果时 3,2,1,0

defer的触发时机

官网说的很清楚:
A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

  1. 包裹着defer语句的函数返回时

  2. 包裹着defer语句的函数执行到最后时

  3. 当前goroutine发生Panic时

        //输出结果:return前执行defer
       func f1() {
           defer fmt.Println("return前执行defer")
           return 
       }
    
       //输出结果:函数执行
       // 函数执行到最后
       func f2() {
           defer fmt.Println("函数执行到最后")
           fmt.Println("函数执行")
       }
    
       //输出结果:panic前  第一个defer在Panic发生时执行,第二个defer在Panic之后声明,不能执行到
       func f3() {
           defer fmt.Println("panic前")
           panic("panic中")
           defer fmt.Println("panic后")
       }

defer,return,返回值的执行顺序

先来看3个例子

func f1() int { //匿名返回值
        var r int = 6
        defer func() {
                r *= 7
        }()
        return r
}

func f2() (r int) { //有名返回值
        defer func() {
                r *= 7
        }()
        return 6
}

func f3() (r int) { //有名返回值
    defer func(r int) {
        r *= 7
    }(r)
    return 6
}

f1的执行结果是6, f2的执行结果是42,f3的执行结果是6
在golang的官方文档里面介绍了,return,defer,返回值的执行顺序:
if the surrounding function returns through an explicit return statement, deferred functions are executed after any result parameters are set by that return statement but before the function returns to its caller.

1. 先给返回值赋值
2. 执行defer语句
3. 包裹函数return返回

f1的结果是6。f1是匿名返回值,匿名返回值是在return执行时被声明,因此defer声明时,还不能访问到匿名返回值,defer的修改不会影响到返回值。
f2先给返回值r赋值,r=6,执行defer语句,defer修改r, r = 42,然后函数return。
f3是有名返回值,但是因为r是作为defer的传参,在声明defer的时候,就进行参数拷贝传递,所以defer只会对defer函数的局部参数有影响,不会影响到调用函数的返回值。

闭包与匿名函数
匿名函数:没有函数名的函数。
闭包:可以使用另外一个函数作用域中的变量的函数。

for i := 0; i <= 3; i++ {
    defer func() {
        fmt.Print(i)
    }
}
//输出结果时 3,3,3,3
因为defer函数的i是对for循环i的引用,defer延迟执行,for循环到最后i是3,到defer执行时i就 
是3

for i := 0; i <= 3; i++ {
    defer func(i int) {
        fmt.Print(i)
    }(i)
}
//输出结果时 3,2,1,0
因为defer函数的i是在defer声明的时候,就当作defer参数传递到defer函数中

defer源码解析
defer的实现源码是在runtime.deferproc
然后在函数返回之前的地方,运行函数runtime.deferreturn。
先了解defer结构体:

    type _defer struct {
            siz     int32 
            started bool
            sp      uintptr // sp at time of defer
            pc      uintptr
            fn      *funcval
            _panic  *_panic // panic that is running defer
            link    *_defer
    }

sp 和 pc 分别指向了栈指针和调用方的程序计数器,fn是向 defer 关键字中传入的函数,Panic是导致运行defer的Panic。
每遇到一个defer关键字,defer函数都会被转换成runtime.deferproc
deferproc通过newdefer创建一个延迟函数,并将这个新建的延迟函数挂在当前goroutine的_defer的链表上

    func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
            sp := getcallersp()
            argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
            callerpc := getcallerpc()

            d := newdefer(siz)
            if d._panic != nil {
                    throw("deferproc: d.panic != nil after newdefer")
            }
            d.fn = fn
            d.pc = callerpc
            d.sp = sp
            switch siz {
            case 0:
                    // Do nothing.
            case sys.PtrSize:
                    *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
            default:
                    memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
            }
            return0()
    }

newdefer会先从sched和当前p的deferpool取出一个_defer结构体,如果deferpool没有_defer,则初始化一个新的_defer。
_defer是关联到当前的g,所以defer只对当前g有效。
d.link = gp._defer
gp._defer = d //用链表连接当前g的所有defer

    func newdefer(siz int32) *_defer {
            var d *_defer
            sc := deferclass(uintptr(siz))
            gp := getg()
            if sc < uintptr(len(p{}.deferpool)) {
                    pp := gp.m.p.ptr()
                    if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil { 
                            .....
                            d := sched.deferpool[sc]
                            sched.deferpool[sc] = d.link
                            d.link = nil
                            pp.deferpool[sc] = append(pp.deferpool[sc], d)
                    }
                    if n := len(pp.deferpool[sc]); n > 0 {
                            d = pp.deferpool[sc][n-1]
                            pp.deferpool[sc][n-1] = nil
                            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
                    }
            }
            ......
            d.siz = siz
            d.link = gp._defer
            gp._defer = d
            return d
    }

deferreturn 从当前g取出_defer链表执行,每个_defer调用freedefer释放_defer结构体,并将该_defer结构体放入当前p的deferpool中。

defer性能分析
defer在开发中,对于资源的释放,捕获Panic等很有用处。可以有些开发者没有考虑过defer对程序性能的影响,在程序中滥用defer。
在性能测试中可以发现,defer对性能还是有一些影响。雨痕的Go 性能优化技巧 4/1,对defer语句带来的额外开销有一些测试。

测试代码

    var mu sync.Mutex
    func noDeferLock() {
        mu.Lock()
        mu.Unlock()
    }   

    func deferLock() {
        mu.Lock()
        defer mu.Unlock()
    }          
    
    func BenchmarkNoDefer(b *testing.B) {
        for i := 0; i < b.N; i++ {
            noDeferLock()
        }
    }
    
    func BenchmarkDefer(b *testing.B) {
        for i := 0; i < b.N; i++ {
            deferLock()
    }

测试结果:

    BenchmarkNoDefer-4      100000000               11.1 ns/op
    BenchmarkDefer-4        36367237                33.1 ns/op

通过前面的源码解析可以知道,defer会先调用deferproc,这些都会进行参数拷贝,deferreturn还会提取相关信息延迟执行,这些都是比直接call一条语句消耗更大。

defer性能不高,每次defer耗时20ns,,在一个func内连续出现多次,性能消耗是20ns*n,累计出来浪费的cpu资源很大的。

解决之道:除了需要异常捕获时,必须使用defer;其它资源回收类defer,可以判断失败后,使用goto跳转到资源回收的代码区。对于竞争资源,可以在使用完之后,立马释放资源,这样才能最优的使用竞争资源。

看完上述内容是否对您有帮助呢?如果还想对相关知识有进一步的了解或阅读更多相关文章,请关注亿速云行业资讯频道,感谢您对亿速云的支持。

推荐阅读:
  1. golang中如何使用defer
  2. Golang函数中defer的作用以及细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

golang

上一篇:JVM的垃圾回收算法工作原理

下一篇:怎么通过重启路由的方法切换IP地址

相关阅读

您好,登录后才能下订单哦!

密码登录
登录注册
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》