溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

GO語言中defer實現(xiàn)原理是什么

發(fā)布時間:2023-02-24 13:54:21 來源:億速云 閱讀:133 作者:iii 欄目:開發(fā)技術(shù)

這篇文章主要介紹“GO語言中defer實現(xiàn)原理是什么”的相關(guān)知識,小編通過實際案例向大家展示操作過程,操作方法簡單快捷,實用性強(qiáng),希望這篇“GO語言中defer實現(xiàn)原理是什么”文章能幫助大家解決問題。

    defer 是什么

    咱們一起來看看 defer 是個啥

    是 GO 中的一個關(guān)鍵字

    這個關(guān)鍵字,我們一般用在釋放資源,在 return 前會調(diào)用他

    如果程序中有多個 defer ,defer 的調(diào)用順序是按照類似的方式,后進(jìn)先出 LIFO的 ,這里順便寫一下

    GO語言中defer實現(xiàn)原理是什么

    遵循后進(jìn)先出原則

    后進(jìn)入棧的,先出棧

    先進(jìn)入棧的,后出棧

    隊列

    GO語言中defer實現(xiàn)原理是什么

    遵循先進(jìn)先出 , 我們就可以想象一個單向的管道,從左邊進(jìn),右邊出

    先進(jìn)來,先出去

    后進(jìn)來,后出去,不準(zhǔn)插隊

    defer 實現(xiàn)原理

    咱們先拋出一個結(jié)論,先心里有點底:

    代碼中聲明 defer的位置,編譯的時候會插入一個函數(shù)叫做 deferproc ,在該defer所在的函數(shù)前插入一個返回的函數(shù),不是return 哦,是deferreturn

    具體的 defer 的實現(xiàn)原理是咋樣的,我們還是一樣的,來看看 defer的底層數(shù)據(jù)結(jié)構(gòu)是啥樣的 ,

    src/runtime/runtime2.gotype _defer struct {結(jié)構(gòu)

    // A _defer holds an entry on the list of deferred calls.
    // If you add a field here, add code to clear it in freedefer and deferProcStack
    // This struct must match the code in cmd/compile/internal/gc/reflect.go:deferstruct
    // and cmd/compile/internal/gc/ssa.go:(*state).call.
    // Some defers will be allocated on the stack and some on the heap.
    // All defers are logically part of the stack, so write barriers to
    // initialize them are not required. All defers must be manually scanned,
    // and for heap defers, marked.
    type _defer struct {
       siz     int32 // includes both arguments and results
       started bool
       heap    bool
       // openDefer indicates that this _defer is for a frame with open-coded
       // defers. We have only one defer record for the entire frame (which may
       // currently have 0, 1, or more defers active).
       openDefer bool
       sp        uintptr  // sp at time of defer
       pc        uintptr  // pc at time of defer
       fn        *funcval // can be nil for open-coded defers
       _panic    *_panic  // panic that is running defer
       link      *_defer
    
       // If openDefer is true, the fields below record values about the stack
       // frame and associated function that has the open-coded defer(s). sp
       // above will be the sp for the frame, and pc will be address of the
       // deferreturn call in the function.
       fd   unsafe.Pointer // funcdata for the function associated with the frame
       varp uintptr        // value of varp for the stack frame
       // framepc is the current pc associated with the stack frame. Together,
       // with sp above (which is the sp associated with the stack frame),
       // framepc/sp can be used as pc/sp pair to continue a stack trace via
       // gentraceback().
       framepc uintptr
    }

    _defer 持有延遲調(diào)用列表中的一個條目 ,我們來看看上述數(shù)據(jù)結(jié)構(gòu)的參數(shù)都是啥意思

    tag說明
    sizdefer函數(shù)的參數(shù)和結(jié)果的內(nèi)存大小
    fn需要被延遲執(zhí)行的函數(shù)
    _panicdefer 的 panic 結(jié)構(gòu)體
    link同一個協(xié)程里面的defer 延遲函數(shù),會通過該指針連接在一起
    heap是否分配在堆上面
    openDefer是否經(jīng)過開放編碼優(yōu)化
    sp棧指針(一般會對應(yīng)到匯編)
    pc程序計數(shù)器

    defer 關(guān)鍵字后面必須是跟函數(shù),這一點咱們要記住哦

    通過上述參數(shù)的描述,我們可以知道,defer的數(shù)據(jù)結(jié)構(gòu)和函數(shù)類似,也是有如下三個參數(shù):

    • 棧指針 SP

    • 程序計數(shù)器 PC

    • 函數(shù)的地址

    可是我們是不是也發(fā)現(xiàn)了,成員里面還有一個link,同一個協(xié)程里面的defer 延遲函數(shù),會通過該指針連接在一起

    這個link指針,是指向的一個defer單鏈表的頭,每次咱們聲明一個defer的時候,就會將該defer的數(shù)據(jù)插入到這個單鏈表頭部的位置,

    那么,執(zhí)行defer的時候,我們是不是就能猜到defer 是咋取得了不?

    前面有說到defer是后進(jìn)先出的,這里當(dāng)然也是遵循這個道理,取defer進(jìn)行執(zhí)行的時候,是從單鏈表的頭開始去取的。

    咱們來畫個圖形象一點

    在協(xié)程A中聲明2defer,先聲明 defer test1()

    GO語言中defer實現(xiàn)原理是什么

    再聲明 defer test2()

    GO語言中defer實現(xiàn)原理是什么

    可以看出后聲明的defer會插入到單鏈表的頭,先聲明的defer被排到后面去了

    咱們?nèi)〉臅r候也是一直取頭下來執(zhí)行,直到單鏈表為空。

    咱一起來看看defer 的具體實現(xiàn)

    GO語言中defer實現(xiàn)原理是什么

    源碼文件在 src/runtime/panic.go 中,查看 函數(shù) deferproc

    // Create a new deferred function fn with siz bytes of arguments.
    // The compiler turns a defer statement into a call to this.
    //go:nosplit
    func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
       gp := getg()
       if gp.m.curg != gp {
          // go code on the system stack can't defer
          throw("defer on system stack")
       }
    
       // the arguments of fn are in a perilous state. The stack map
       // for deferproc does not describe them. So we can't let garbage
       // collection or stack copying trigger until we've copied them out
       // to somewhere safe. The memmove below does that.
       // Until the copy completes, we can only call nosplit routines.
       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.link = gp._defer
       gp._defer = d
       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))
       }
    
       // 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.
    }

    deferproc 的作用是

    創(chuàng)建一個新的遞延函數(shù) fn,參數(shù)為 siz 字節(jié),編譯器將一個延遲語句轉(zhuǎn)換為對this的調(diào)用

    getcallersp()

    得到deferproc之前的rsp寄存器的值,實現(xiàn)的方式所有平臺都是一樣的

    //go:noescape
    func getcallersp() uintptr // implemented as an intrinsic on all platforms

    callerpc := getcallerpc()

    此處得到 rsp之后,存儲在 callerpc 中 , 此處是為了調(diào)用 deferproc 的下一條指令

    d := newdefer(siz)

    d := newdefer(siz) 新建一個defer 的結(jié)構(gòu),后續(xù)的代碼是在給defer 這個結(jié)構(gòu)的成員賦值

    咱看看 deferproc 的大體流程

    • 獲取 deferproc之前的rsp寄存器的值

    • 使用newdefer 分配一個 _defer 結(jié)構(gòu)體對象,并且將他放到當(dāng)前的 _defer 鏈表的頭

    • 初始化_defer 的相關(guān)成員參數(shù)

    • return0

    來我們看看 newdefer的源碼

    源碼文件在 src/runtime/panic.go 中,查看函數(shù)newdefer

    // Allocate a Defer, usually using per-P pool.
    // Each defer must be released with freedefer.  The defer is not
    // added to any defer chain yet.
    //
    // This must not grow the stack because there may be a frame without
    // stack map information when this is called.
    //
    //go:nosplit
    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 {
    			// Take the slow path on the system stack so
    			// we don't grow newdefer's stack.
    			systemstack(func() {
    				lock(&sched.deferlock)
    				for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
    					d := sched.deferpool[sc]
    					sched.deferpool[sc] = d.link
    					d.link = nil
    					pp.deferpool[sc] = append(pp.deferpool[sc], d)
    				}
    				unlock(&sched.deferlock)
    			})
    		}
    		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 {
    		// Allocate new defer+args.
    		systemstack(func() {
    			total := roundupsize(totaldefersize(uintptr(siz)))
    			d = (*_defer)(mallocgc(total, deferType, true))
    		})
    	}
    	d.siz = siz
    	d.heap = true
    	return d
    }

    newderfer 的作用:

    通常使用per-P池,分配一個Defer

    每個defer可以自由的釋放。當(dāng)前defer 也不會加入任何一個 defer鏈條中

    getg()

    獲取當(dāng)前協(xié)程的結(jié)構(gòu)體指針

    // getg returns the pointer to the current g.
    // The compiler rewrites calls to this function into instructions
    // that fetch the g directly (from TLS or from the dedicated register).
    func getg() *g

    pp := gp.m.p.ptr()

    拿到當(dāng)前工作線程里面的 P

    然后拿到 從全局的對象池子中拿一部分對象給到P的池子里面

    for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
    					d := sched.deferpool[sc]
    					sched.deferpool[sc] = d.link
    					d.link = nil
    					pp.deferpool[sc] = append(pp.deferpool[sc], d)
    				}

    GO語言中defer實現(xiàn)原理是什么

    點進(jìn)去看池子的數(shù)據(jù)結(jié)構(gòu),其實里面的成員也就是 咱們之前說到的 _defer指針

    其中 sched.deferpool[sc] 是全局的池子,pp.deferpool[sc] 是本地的池子

    mallocgc分配空間

    上述操作若 d 沒有拿到值,那么就直接使用 mallocgc 重新分配,且設(shè)置好 對應(yīng)的成員 sizheap

    if d == nil {
    		// Allocate new defer+args.
    		systemstack(func() {
    			total := roundupsize(totaldefersize(uintptr(siz)))
    			d = (*_defer)(mallocgc(total, deferType, true))
    		})
    	}
    d.siz = siz
    d.heap = true

    mallocgc 具體實現(xiàn)在 src/runtime/malloc.go 中,若感興趣的話,可以深入看看這一塊,今天咱們不重點說這個函數(shù)

    // Allocate an object of size bytes.
    // Small objects are allocated from the per-P cache's free lists.
    // Large objects (> 32 kB) are allocated straight from the heap.
    func mallocgc(size uintptr, typ *_type, needzero bool) unsafe.Pointer {}

    最后再來看看return0

    最后再來看看 deferproc 函數(shù)中的 結(jié)果返回return0()

    // return0 is a stub used to return 0 from deferproc.
    // It is called at the very end of deferproc to signal
    // the calling Go function that it should not jump
    // to deferreturn.
    // in asm_*.s
    func return0()

    return0 是用于從deferproc返回0的存根

    它在deferproc函數(shù)的最后被調(diào)用,用來通知調(diào)用Go的函數(shù)它不應(yīng)該跳轉(zhuǎn)到deferreturn。

    在正常情況下 return0 正常返回 0

    可是異常情況下 return0 函數(shù)會返回 1,此時GO 就會跳轉(zhuǎn)到執(zhí)行 deferreturn

    簡單說下 deferreturn

    deferreturn的作用就是情況defer里面的鏈表,歸還相應(yīng)的緩沖區(qū),或者把對應(yīng)的空間讓GC回收調(diào)

    GO 中 defer 的規(guī)則

    上面分析了GO 中defer 的實現(xiàn)原理之后,咱們現(xiàn)在來了解一下 GO 中應(yīng)用defer 是需要遵守 3 個規(guī)則的,咱們來列一下:

    • defer后面跟的函數(shù),叫延遲函數(shù),函數(shù)中的參數(shù)在defer語句聲明的時候,就已經(jīng)確定下來了

    • 延遲函數(shù)的執(zhí)行時按照后進(jìn)先出來的,文章前面也多次說到過,這個印象應(yīng)該很深刻吧,先出現(xiàn)的defer后執(zhí)行,后出現(xiàn)的defer先執(zhí)行

    • 延遲函數(shù)可能會影響到整個函數(shù)的返回值

    咱們還是要來解釋一下的,上面第 2 點,應(yīng)該都好理解,上面的圖也表明了 執(zhí)行順序

    第一點咱們來寫個小DEMO

    延遲函數(shù)中的參數(shù)在defer語句聲明的時候,就已經(jīng)確定下來了

    func main() {
       num := 1
       defer fmt.Println(num)
    
       num++
    
       return
    }

    別猜了,運(yùn)行結(jié)果是 1,小伙伴們可以將代碼拷貝下來,自己運(yùn)行一波

    第三點也來一個DEMO

    延遲函數(shù)可能會影響到整個函數(shù)的返回值

    func test3() (res int) {
       defer func() {
          res++
       }()
    
       return 1
    }
    func main() {
    
       fmt.Println(test3())
    
       return
    }

    上述代碼,我們在 test3函數(shù)中的返回值,我們提前命名好了,本來應(yīng)該是返回結(jié)果為 1

    可是在return 這里,執(zhí)行順序這樣的

    res = 1

    res++

    因此,結(jié)果就是 2

    關(guān)于“GO語言中defer實現(xiàn)原理是什么”的內(nèi)容就介紹到這里了,感謝大家的閱讀。如果想了解更多行業(yè)相關(guān)的知識,可以關(guān)注億速云行業(yè)資訊頻道,小編每天都會為大家更新不同的知識點。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI