Golang學(xué)習(xí)筆記-defer關(guān)鍵字學(xué)習(xí)

defer學(xué)習(xí)

很多現(xiàn)代的變成語言中都會(huì)有defer關(guān)鍵字,Go語言的defer會(huì)在當(dāng)前函數(shù)或是方法返回之前執(zhí)行傳入的函數(shù),它會(huì)經(jīng)常被用于
關(guān)閉文件描述符,關(guān)閉數(shù)據(jù)庫鏈接和解鎖資源。
作為一個(gè)編程語言中的關(guān)鍵字,defer 的實(shí)現(xiàn)一定是由編譯器和運(yùn)行時(shí)共同完成的,
不過在深入源碼分析它的實(shí)現(xiàn)之前我們還是需要了解 defer 關(guān)鍵字的常見使用場(chǎng)景以及使用時(shí)的注意事項(xiàng)。

  1. 使用defer的最常見的場(chǎng)景就是在函數(shù)調(diào)用結(jié)束的時(shí)候完成一些收尾工作,比如在defer中回滾數(shù)據(jù)庫
func createPost(db *gorm.DB) error{
    tx := db.Begin()
    defer tx.Rollback()
    if err := tx.Create(&Post{Author: "Draveness"}).Error; err != nil{
        return err
    }
    return tx.Commit().Error

}

現(xiàn)象

我們?cè)?Go 語言中使用 defer 時(shí)會(huì)遇到兩個(gè)比較常見的問題,這里會(huì)介紹具體的場(chǎng)景并分析這兩個(gè)現(xiàn)象背后的設(shè)計(jì)原理:

  1. defer 關(guān)鍵字的調(diào)用時(shí)機(jī)以及多次調(diào)用 defer 時(shí)執(zhí)行順序是如何確定的;
  2. defer 關(guān)鍵字使用傳值的方式傳遞參數(shù)時(shí)會(huì)進(jìn)行預(yù)計(jì)算,導(dǎo)致不符合預(yù)期的結(jié)果;

作用域

向 defer 關(guān)鍵字傳入的函數(shù)會(huì)在函數(shù)返回之前運(yùn)行。假設(shè)我們?cè)?for 循環(huán)中多次調(diào)用 defer 關(guān)鍵字:
defer的執(zhí)行順序是從后向前執(zhí)行,先定義后執(zhí)行

func main(){
    for i:=0;i<5;i++{
        defer fmt.Println(i)
    }
}
打印結(jié)果為:4,3,2,1

defer 傳入的函數(shù)不是在退出代碼塊的作用域時(shí)執(zhí)行的,它只會(huì)在當(dāng)前函數(shù)和方法返回之前被調(diào)用。

func main(){
    {
        defer fmt.Println("defer runs")
        fmt.Println("block ends")
    }
    fmt.Println("main ends")
}
執(zhí)行結(jié)果為:
block ends
main ends
defer runs           

預(yù)計(jì)算參數(shù)

Go 語言中所有的函數(shù)調(diào)用都是傳值的,defer 雖然是關(guān)鍵字,但是也繼承了這個(gè)特性。假設(shè)我們想要計(jì)算 main 函數(shù)運(yùn)行的時(shí)間,可能會(huì)寫出以下的代碼

func main() {
    startedAt := time.Now()
    defer fmt.Println(time.Since(startedAt))
    time.Sleep(time.Second)
}
執(zhí)行結(jié)果為0s

調(diào)用 defer 關(guān)鍵字會(huì)立刻對(duì)函數(shù)中引用的外部參數(shù)進(jìn)行拷貝,所以 time.Since(startedAt) 的結(jié)果不是在 main 函數(shù)退出之前計(jì)算的,
而是在 defer 關(guān)鍵字調(diào)用時(shí)計(jì)算的,最終導(dǎo)致上述代碼輸出 0s。解決這個(gè)方法只通過傳參數(shù)的方式就可以解決。

數(shù)據(jù)結(jié)構(gòu)

defer 關(guān)鍵字在 Go 語言源代碼中對(duì)應(yīng)的數(shù)據(jù)結(jié)構(gòu):

type _defer struct {
    siz     int32
    started bool
    sp      uintptr
    pc      uintptr
    fn      *funcval
    _panic  *_panic
    link    *_defer
}

runtime._defer 結(jié)構(gòu)體是延遲調(diào)用鏈表上的一個(gè)元素,所有結(jié)構(gòu)體都會(huì)通過link字段串鏈成鏈表

  1. siz 是參數(shù)和結(jié)果的內(nèi)存大小
  2. sp和pc分別代表?xiàng)V羔樅驼{(diào)用方的程序技術(shù)器
  3. fn是defer關(guān)鍵字中傳入的函數(shù)
  4. _panic 是觸發(fā)延遲調(diào)用的結(jié)構(gòu)體

編譯過程

defer 關(guān)鍵字在運(yùn)行期間會(huì)調(diào)用 runtime.deferproc 函數(shù),這個(gè)函數(shù)接收了參數(shù)的大小和閉包所在的地址兩個(gè)參數(shù)。
編譯器不僅將 defer 關(guān)鍵字都轉(zhuǎn)換成 runtime.deferproc 函數(shù),它還會(huì)通過以下三個(gè)步驟為所有調(diào)用
defer 的函數(shù)末尾插入 runtime.deferreturn 的函數(shù)調(diào)用:

  1. cmd/compile/internal/gc.walkstmt 在遇到 ODEFER 節(jié)點(diǎn)時(shí)會(huì)執(zhí)行 Curfn.Func.SetHasDefer(true) 設(shè)置當(dāng)前函數(shù)的 hasdefer;
  2. cmd/compile/internal/gc.buildssa 會(huì)執(zhí)行 s.hasdefer = fn.Func.HasDefer() 更新 state 的 hasdefer;
  3. cmd/compile/internal/gc.state.exit 會(huì)根據(jù) state 的 hasdefer 在函數(shù)返回之前插入 runtime.deferreturn 的函數(shù)調(diào)用;

編譯過程

defer 關(guān)鍵字的運(yùn)行時(shí)實(shí)現(xiàn)了分成兩部分

  1. runtime.deferproc 函數(shù)負(fù)責(zé)創(chuàng)建新的延遲調(diào)用
  2. runtime.deferreturn 函數(shù)負(fù)責(zé)在函數(shù)調(diào)用結(jié)束時(shí)執(zhí)行所有的延遲調(diào)用

創(chuàng)建延遲調(diào)用

runtime.deferproc 會(huì)為 defer 創(chuàng)建一個(gè)新的 runtime._defer 結(jié)構(gòu)體、設(shè)置它的函數(shù)指針 fn、
程序計(jì)數(shù)器 pc 和棧指針 sp 并將相關(guān)的參數(shù)拷貝到相鄰的內(nèi)存空間中:

func deferproc(siz int32,fn *funcval){
    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:
    case sys.PtrSize:
        *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
    default:
        memmove(deferArgs(d),unsafe.Pointer(argp),uintptr(siz))
    }
    return0()
}

最后調(diào)用的 runtime.return0 函數(shù)的作用是避免無限遞歸調(diào)用 runtime.deferreturn,它是唯一一個(gè)不會(huì)觸發(fā)由延遲調(diào)用的函數(shù)了。

runtime.deferproc 中 runtime.newdefer 的作用就是想盡辦法獲得一個(gè) runtime._defer 結(jié)構(gòu)體,辦法總共有三個(gè):

  1. 從調(diào)度器的延遲調(diào)用緩存池 sched.deferpool 中取出結(jié)構(gòu)體并將該結(jié)構(gòu)體追加到當(dāng)前的Goroutine的緩存池中
  2. 從Goroutine的延遲調(diào)用緩存池的pp.deferpool中取出結(jié)構(gòu)體
  3. 通過runtime.mallocgc創(chuàng)建一個(gè)新的結(jié)構(gòu)體
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 {
            for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
                d := sched.deferpool[sc]
                sched.deferpool[sc] = d.link
                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]
        }
    }
    if d == nil {
        total := roundupsize(totaldefersize(uintptr(siz)))
        d = (*_defer)(mallocgc(total, deferType, true))
    }
    d.siz = siz
    d.link = gp._defer
    gp._defer = d
    return d
}

無論使用哪種方式獲取 runtime._defer,它都會(huì)被追加到所在的 Goroutine _defer 鏈表的最前面。

defer 關(guān)鍵字插入時(shí)是從后向前的,而 defer 關(guān)鍵字執(zhí)行是從前向后的,而這就是后調(diào)用的 defer 會(huì)優(yōu)先執(zhí)行的原因。

執(zhí)行延遲調(diào)用

runtime.deferreturn 函數(shù)會(huì)多次判斷當(dāng)前 Goroutine 的 _defer 鏈表中是否有未執(zhí)行的剩余結(jié)構(gòu),
在所有的延遲函數(shù)調(diào)用都執(zhí)行完成之后,該函數(shù)才會(huì)返回。

小結(jié)

defer 關(guān)鍵字的實(shí)現(xiàn)主要是依靠編譯器和運(yùn)行時(shí)的協(xié)作

  1. 編譯期
    1. 將defer 關(guān)鍵字被替換成runtime.deferproc
    2. 在調(diào)用defer關(guān)鍵字的函數(shù)返回之前插入runtime.deferreturn
  2. 運(yùn)行時(shí)
    1. runtime.deferproc 會(huì)將一個(gè)新的 runtime._defer 結(jié)構(gòu)體追加到當(dāng)前 Goroutine 的鏈表頭;
    2. runtime.deferreturn 會(huì)從 Goroutine 的鏈表中取出 runtime._defer 結(jié)構(gòu)并依次執(zhí)行;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容