在上一章節 《深刻理解 Go panic and recover》 中,咱們發現了 defer
與其關聯性極大,仍是以爲很是有必要深刻一下。但願經過本章節你們能夠對 defer
關鍵字有一個深入的理解,那麼咱們開始吧。你先等等,請排好隊,咱們這兒採起後進先出 LIFO 的出站方式...html
原文地址:深刻理解 Go defergit
咱們簡單的過一下 defer
關鍵字的基礎使用,讓你們先有一個基礎的認知github
func main() { defer log.Println("EDDYCJY.") log.Println("end.") }
輸出結果:golang
$ go run main.go 2019/05/19 21:15:02 end. 2019/05/19 21:15:02 EDDYCJY.
func main() { for i := 0; i < 6; i++ { defer log.Println("EDDYCJY" + strconv.Itoa(i) + ".") } log.Println("end.") }
輸出結果:segmentfault
$ go run main.go 2019/05/19 21:19:17 end. 2019/05/19 21:19:17 EDDYCJY5. 2019/05/19 21:19:17 EDDYCJY4. 2019/05/19 21:19:17 EDDYCJY3. 2019/05/19 21:19:17 EDDYCJY2. 2019/05/19 21:19:17 EDDYCJY1. 2019/05/19 21:19:17 EDDYCJY0.
func main() { func() { defer log.Println("defer.EDDYCJY.") }() log.Println("main.EDDYCJY.") }
輸出結果:數據結構
$ go run main.go 2019/05/22 23:30:27 defer.EDDYCJY. 2019/05/22 23:30:27 main.EDDYCJY.
func main() { defer func() { if e := recover(); e != nil { log.Println("EDDYCJY.") } }() panic("end.") }
輸出結果:less
$ go run main.go 2019/05/20 22:22:57 EDDYCJY.
$ go tool compile -S main.go "".main STEXT size=163 args=0x0 locals=0x40 ... 0x0059 00089 (main.go:6) MOVQ AX, 16(SP) 0x005e 00094 (main.go:6) MOVQ $1, 24(SP) 0x0067 00103 (main.go:6) MOVQ $1, 32(SP) 0x0070 00112 (main.go:6) CALL runtime.deferproc(SB) 0x0075 00117 (main.go:6) TESTL AX, AX 0x0077 00119 (main.go:6) JNE 137 0x0079 00121 (main.go:7) XCHGL AX, AX 0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) 0x007f 00127 (main.go:7) MOVQ 56(SP), BP 0x0084 00132 (main.go:7) ADDQ $64, SP 0x0088 00136 (main.go:7) RET 0x0089 00137 (main.go:6) XCHGL AX, AX 0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB) 0x008f 00143 (main.go:6) MOVQ 56(SP), BP 0x0094 00148 (main.go:6) ADDQ $64, SP 0x0098 00152 (main.go:6) RET ...
首先咱們須要找到它,找到它實際對應什麼執行代碼。經過彙編代碼,可得知涉及以下方法:函數
很顯然是運行時的方法,是對的人。咱們繼續往下走看看都分別承擔了什麼行爲ui
在開始前咱們須要先介紹一下 defer
的基礎單元 _defer
結構體,以下:spa
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 } ... type funcval struct { fn uintptr // variable-size, fn-specific data here }
defer
是否已經執行過_panic
鏈表_defer
鏈表
func deferproc(siz int32, fn *funcval) { ... sp := getcallersp() argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) callerpc := getcallerpc() d := newdefer(siz) ... 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() }
defer
函數的函數棧指針、傳入函數的參數具體地址以及PC (程序計數器),也就是下一個要執行的指令。這些至關因而預備參數,便於後續的流轉控制defer
最小單元 _defer
,填入先前準備的參數memmove
將傳入的參數存儲到新 _defer
(當前使用)中去,便於後續的使用return0
進行返回,這個函數很是重要。可以避免在 deferproc
中又由於返回 return
,而誘發 deferreturn
方法的調用。其根本緣由是一箇中止 panic
的延遲方法會使 deferproc
返回 1,但在機制中若是 deferproc
返回不等於 0,將會老是檢查返回值並跳轉到函數的末尾。而 return0
返回的就是 0,所以能夠防止重複調用在這個函數中會爲新的 _defer
設置一些基礎屬性,並將調用函數的參數集傳入。最後經過特殊的返回方法結束函數調用。另外這一塊與先前 《深刻理解 Go panic and recover》 的處理邏輯有必定關聯性,其實就是 gp.sched.ret
返回 0 仍是 1 會分流至不一樣處理方式
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 { ... lock(&sched.deferlock) d := sched.deferpool[sc] unlock(&sched.deferlock) } ... } if d == nil { systemstack(func() { total := roundupsize(totaldefersize(uintptr(siz))) d = (*_defer)(mallocgc(total, deferType, true)) }) ... } d.siz = siz d.link = gp._defer gp._defer = d return d }
_defer
,則複用做爲新的基礎單元mallocgc
從新申請一個新的defer
的基礎屬性,最後修改當前 Goroutine
的 _defer
指向經過這個方法咱們能夠注意到兩點,以下:
defer
與 Goroutine(g)
有直接關係,因此討論 defer
時基本離不開 g
的關聯defer
老是會在現有的鏈表中的最前面,也就是 defer
的特性後進先出這個函數主要承擔了獲取新的 _defer
的做用,它有多是從 deferpool
中獲取的,也有多是從新申請的
func deferreturn(arg0 uintptr) { gp := getg() d := gp._defer if d == nil { return } sp := getcallersp() if d.sp != sp { return } switch d.siz { case 0: // Do nothing. case sys.PtrSize: *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d)) default: memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz)) } fn := d.fn d.fn = nil gp._defer = d.link freedefer(d) jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) }
若是在一個方法中調用過 defer
關鍵字,那麼編譯器將會在結尾處插入 deferreturn
方法的調用。而該方法中主要作了以下事項:
_defer
被調用的函數調用信息_defer
的存儲信息並放回池中(便於複用)defer
關鍵字的調用函數處在這段代碼中,跳轉方法 jmpdefer
格外重要。由於它顯式的控制了流轉,代碼以下:
// asm_amd64.s TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16 MOVQ fv+0(FP), DX // fn MOVQ argp+8(FP), BX // caller sp LEAQ -8(BX), SP // caller sp after CALL MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use) SUBQ $5, (SP) // return to CALL again MOVQ 0(DX), BX JMP BX // but first run the deferred function
經過源碼的分析,咱們發現它作了兩個很 「奇怪」 又很重要的事,以下:
-8(BX)
這個位置保存的是 deferreturn
執行完畢後的地址SP
的地址減 5 ,其減掉的長度就剛好是 runtime.deferreturn
的長度你可能會問,爲何是 5?好吧。翻了半天最後看了一下彙編代碼...嗯,相減的確是 5 沒毛病,以下:
0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) 0x007f 00127 (main.go:7) MOVQ 56(SP), BP
咱們整理一下思緒,照上述邏輯的話,那 deferreturn
就是一個 「遞歸」 了哦。每次都會從新回到 deferreturn
函數,那它在何時纔會結束呢,以下:
func deferreturn(arg0 uintptr) { gp := getg() d := gp._defer if d == nil { return } ... }
也就是會不斷地進入 deferreturn
函數,判斷鏈表中是否還存着 _defer
。若已經不存在了,則返回,結束掉它。簡單來說,就是處理徹底部 defer
才容許你真的離開它。果然如此嗎?咱們再看看上面的彙編代碼,以下:
。.. 0x0070 00112 (main.go:6) CALL runtime.deferproc(SB) 0x0075 00117 (main.go:6) TESTL AX, AX 0x0077 00119 (main.go:6) JNE 137 0x0079 00121 (main.go:7) XCHGL AX, AX 0x007a 00122 (main.go:7) CALL runtime.deferreturn(SB) 0x007f 00127 (main.go:7) MOVQ 56(SP), BP 0x0084 00132 (main.go:7) ADDQ $64, SP 0x0088 00136 (main.go:7) RET 0x0089 00137 (main.go:6) XCHGL AX, AX 0x008a 00138 (main.go:6) CALL runtime.deferreturn(SB) ...
的確如上述流程所分析一致,驗證完畢
這個函數主要承擔了清空已使用的 defer
和跳轉到調用 defer
關鍵字的函數處,很是重要
咱們有提到 defer
關鍵字涉及兩個核心的函數,分別是 deferproc
和 deferreturn
函數。而 deferreturn
函數比較特殊,是當應用函數調用 defer
關鍵字時,編譯器會在其結尾處插入 deferreturn
的調用,它們倆通常都是成對出現的
可是當一個 Goroutine
上存在着屢次 defer
行爲(也就是多個 _defer
)時,編譯器會進行利用一些小技巧, 從新回到 deferreturn
函數去消耗 _defer
鏈表,直到一個不剩才容許真正的結束
而新增的基礎單元 _defer
,有多是被複用的,也有多是全新申請的。它最後都會被追加到 _defer
鏈表的表頭,從而設定了後進先出的調用特性