首頁  >  文章  >  後端開發  >  一文詳解golang defer的實作原理

一文詳解golang defer的實作原理

藏色散人
藏色散人轉載
2021-09-09 15:22:582572瀏覽

本文由go語言教學專欄為大家介紹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 #包裹著defer語句的函數執行到最後時
  2. 當前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 state state turn state statement .

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跳到資源回收的程式碼區。對於競爭資源,可以在使用完之後,立刻釋放資源,這樣才能最優的使用競爭資源。

更多golang相關知識,請造訪golang教學欄位!

以上是一文詳解golang defer的實作原理的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:segmentfault.com。如有侵權,請聯絡admin@php.cn刪除