Go defer 原理和源碼剖析(推薦)
Go 語言中有一個非常有用的保留字 defer,它可以調(diào)用一個函數(shù),該函數(shù)的執(zhí)行被推遲到包裹它的函數(shù)返回時執(zhí)行。
defer 語句調(diào)用的函數(shù),要么是因為包裹它的函數(shù)執(zhí)行了 return 語句,到達(dá)了函數(shù)體的末端,要么是因為對應(yīng)的 goroutine 發(fā)生了 panic。
在實際的 go 語言程序中,defer 語句可以代替其它語言中 try…catch… 的作用,也可以用來處理釋放資源等收尾操作,比如關(guān)閉文件句柄、關(guān)閉數(shù)據(jù)庫連接等。
1. 編譯器編譯 defer 過程
defer dosomething(x)
簡單來說,執(zhí)行 defer 語句,實際上是注冊了一個稍后執(zhí)行的函數(shù),確定了函數(shù)名和參數(shù),但不會立即調(diào)用,而是把調(diào)用過程推遲到當(dāng)前函數(shù) return 或者發(fā)生 panic 的時候。
我們先了解一下 defer 相關(guān)的數(shù)據(jù)結(jié)構(gòu)。
1) struct _defer 數(shù)據(jù)結(jié)構(gòu)
go 語言程序中每一次調(diào)用 defer 都生成一個 _defer 結(jié)構(gòu)體。
type _defer struct { siz int32 // 參數(shù)和返回值的內(nèi)存大小 started boul heap boul // 區(qū)分該結(jié)構(gòu)是在棧上分配的,還是對上分配的 sp uintptr // sp 計數(shù)器值,棧指針; pc uintptr // pc 計數(shù)器值,程序計數(shù)器; fn *funcval // defer 傳入的函數(shù)地址,也就是延后執(zhí)行的函數(shù); _panic *_panic // panic that is running defer link *_defer // 鏈表 }
我們默認(rèn)使用了 go 1.13 版本的源代碼,其它版本類似。
一個函數(shù)內(nèi)可以有多個 defer 調(diào)用,所以自然需要一個數(shù)據(jù)結(jié)構(gòu)來組織這些 _defer 結(jié)構(gòu)體。_defer 按照對齊規(guī)則占用 48 字節(jié)的內(nèi)存。在 _defer 結(jié)構(gòu)體中的 link 字段,這個字段把所有的 _defer 串成一個鏈表,表頭是掛在 Goroutine 的 _defer 字段。
_defer 的鏈?zhǔn)浇Y(jié)構(gòu)如下:
_defer.siz 用于指定延遲函數(shù)的參數(shù)和返回值的空間,大小由 _defer.siz 指定,這塊內(nèi)存的值在 defer 關(guān)鍵字執(zhí)行的時候填充好。
defer 延遲函數(shù)的參數(shù)是預(yù)計算的,在棧上分配空間。每一個 defer 調(diào)用在棧上分配的內(nèi)存布局如下圖所示:
其中 _defer 是一個指針,指向一個 struct _defer 對象,它可能分配在棧上,也可能分配在堆上。
2) struct _defer 內(nèi)存分配
以下是一個使用 defer 的范例,文件名為 test_defer.go:
package main func doDeferFunc(x int) { println(x) } func doSomething() int { var x = 1 defer doDeferFunc(x) x += 2 return x } func main() { x := doSomething() println(x) }
編譯以上代碼,加上去除優(yōu)化和內(nèi)鏈選項:
go tool compile -N -l test_defer.go
導(dǎo)出匯編代碼:
go tool objdump test_defer.o
我們看下編譯成的二進(jìn)制代碼:
從匯編指令我們看到,編譯器在遇到 defer 關(guān)鍵字的時候,添加了一些運行庫函數(shù):deferprocStack
和deferreturn
。
go 1.13 正式版本的發(fā)布提升了 defer 的性能,號稱針對 defer 場景提升了 30% 的性能。
go 1.13 之前的版本 defer 語句會被編譯器翻譯成兩個過程:回調(diào)注冊函數(shù)過程:deferproc
和deferreturn
。
go 1.13 帶來的 deferprocStack 函數(shù),這個函數(shù)就是這個 30% 性能提升的核心手段。deferprocStack 和 deferproc 的目的都是注冊回調(diào)函數(shù),但是不同的是 deferprocStatck 是在棧內(nèi)存上分配 struct _defer 結(jié)構(gòu),而 deferproc 這個是需要去堆上分配結(jié)構(gòu)內(nèi)存的。而我們絕大部分的場景都是可以是在棧上分配的,所以自然整體性能就提升了。棧上分配內(nèi)存自然是比對上要快太多了,只需要改變 rsp 寄存器的值就可以進(jìn)行分配。
那么什么時候分配在棧上,什么時候分配在堆上呢?
在編譯器相關(guān)的文件(src/cmd/compile/internal/gc/ssa.go )里,有個條件判斷:
func (s *state) stmt(n *Node) { case ODEFER: d := callDefer if n.Esc == EscNever { d = callDeferStack } }
n.Esc 是 ast.Node 的逃逸分析的結(jié)果,那么什么時候 n.Esc 會被置成 EscNever 呢?
這個在逃逸分析的函數(shù) esc 里(src/cmd/compile/internal/gc/esc.go ):
func (e *EscState) esc(n *Node, parent *Node) { case ODEFER: if e.loopdepth == 1 { // top level n.Esc = EscNever // force stack allocation of defer record (see ssa.go) break } }
這里 e.loopdepth 等于 1的時候,才會設(shè)置成 EscNever ,e.loopdepth 字段是用于檢測嵌套循環(huán)作用域的,換句話說,defer 如果在嵌套作用域的上下文中,那么就可能導(dǎo)致 struct _defer 分配在堆上,如下:
package main func main() { for i := 0; i < 10; i++ { defer func() { _ = i }() } }
編譯器生成的則是 deferproc :
當(dāng) defer 外層出現(xiàn)顯式(for)或者隱式(goto)的時候,將會導(dǎo)致 struct _defer 結(jié)構(gòu)體分配在堆上,性能就會變差,這個編程的時候要注意。
編譯器就能決定 _defer 結(jié)構(gòu)體分配在棧上還是堆上,對應(yīng)函數(shù)分別是 deferprocStatck 和 deferproc 函數(shù),這兩個函數(shù)都很簡單,目的一致:分配出 struct _defer 的內(nèi)存結(jié)構(gòu),把回調(diào)函數(shù)初始化進(jìn)去,掛到鏈表中。
3) deferprocStack 棧上分配
deferprocStack 函數(shù)做了哪些事情呢?
// 進(jìn)入這個函數(shù)之前,就已經(jīng)在棧上分配好了內(nèi)存結(jié)構(gòu) func deferprocStack(d *_defer) { gp := getg() // siz 和 fn 在進(jìn)入這個函數(shù)之前已經(jīng)賦值 d.started = false // 表明是棧的內(nèi)存 d.heap = false // 獲取到 caller 函數(shù)的 rsp 寄存器值,并賦值到 _defer 結(jié)構(gòu) sp 字段中 d.sp = getcallersp() // 獲取到 caller 函數(shù)的 rip 寄存器值,并賦值到 _defer 結(jié)構(gòu) pc 字段中 // 根據(jù)函數(shù)調(diào)用的原理,我們就知道 caller 的壓棧的 pc (rip) 值就是 deferprocStack 的下一條指令 d.pc = getcallerpc() // 把這個 _defer 結(jié)構(gòu)作為一個節(jié)點,掛到 goroutine 的鏈表中 *(*uintptr)(unsafe.Pointer(&d._panic)) = 0 *(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer)) *(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d)) // 注意,特殊的返回,不會觸發(fā)延遲調(diào)用的函數(shù) return0() }
小結(jié):
- 由于是棧上分配內(nèi)存的,所以調(diào)用到 deferprocStack 之前,編譯器就已經(jīng)把 struct _defer 結(jié)構(gòu)的函數(shù)準(zhǔn)備好了;
- _defer.heap 字段用來標(biāo)識這個結(jié)構(gòu)體分配在棧上;
- 保存上下文,把 caller 函數(shù)的 rsp,pc(rip) 寄存器的值保存到 _defer 結(jié)構(gòu)體;
- _defer 作為一個節(jié)點掛接到鏈表。注意:表頭是 goroutine 結(jié)構(gòu)的 _defer 字段,而在一個協(xié)程任務(wù)中大部分有多次函數(shù)調(diào)用的,所以這個鏈表會掛接一個調(diào)用棧上的 _defer 結(jié)構(gòu),執(zhí)行的時候按照 rsp 來過濾區(qū)分;4) deferproc 堆上分配
堆上分配的函數(shù)為 deferproc ,簡化邏輯如下:
func deferproc(siz int32, fn *funcval) { // arguments of fn fullow fn // 獲取 caller 函數(shù)的 rsp 寄存器值 sp := getcallersp() argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn) // 獲取 caller 函數(shù)的 pc(rip) 寄存器值 callerpc := getcallerpc() // 分配 struct _defer 內(nèi)存結(jié)構(gòu) d := newdefer(siz) if d._panic != nil { throw("deferproc: d.panic != nil after newdefer") } // _defer 結(jié)構(gòu)體初始化 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)) } // 注意,特殊的返回,不會觸發(fā)延遲調(diào)用的函數(shù) return0() }
小結(jié):
- 與棧上分配不同,struct _defer 結(jié)構(gòu)是在該函數(shù)里分配的,調(diào)用 newdefer 分配結(jié)構(gòu)體,newdefer 函數(shù)則是先去 poul 緩存池里看一眼,有就直接取用,沒有就調(diào)用 mallocgc 從堆上分配內(nèi)存;
- deferproc 接受入?yún)?siz,fn ,這兩個參數(shù)分別標(biāo)識延遲函數(shù)的參數(shù)和返回值的內(nèi)存大小,延遲函數(shù)地址;
- _defer.heap 字段用來標(biāo)識這個結(jié)構(gòu)體分配在堆上;
- 保存上下文,把 caller 函數(shù)的 rsp,pc(rip) 寄存器的值保存到 _defer 結(jié)構(gòu)體;
- _defer 作為一個節(jié)點掛接到鏈表;
5) 執(zhí)行 defer 函數(shù)鏈
編譯器遇到 defer 語句,會插入兩個函數(shù):
- 分配函數(shù):deferproc 或者 deferprocStack ;
- 執(zhí)行函數(shù):deferreturn 。
包裹 defer 語句的函數(shù)退出的時候,由 deferreturn 負(fù)責(zé)執(zhí)行所有的延遲調(diào)用鏈。
func deferreturn(arg0 uintptr) { gp := getg() // 獲取到最前的 _defer 節(jié)點 d := gp._defer // 函數(shù)遞歸終止條件(d 鏈表遍歷完成) if d == nil { return } // 獲取 caller 函數(shù)的 rsp 寄存器值 sp := getcallersp() if d.sp != sp { // 如果 _defer.sp 和 caller 的 sp 值不一致,那么直接返回; // 因為,就說明這個 _defer 結(jié)構(gòu)不是在該 caller 函數(shù)注冊的 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)) } // 獲取到延遲回調(diào)函數(shù)地址 fn := d.fn d.fn = nil // 把當(dāng)前 _defer 節(jié)點從鏈表中摘除 gp._defer = d.link // 釋放 _defer 內(nèi)存(主要是堆上才會需要處理,棧上的隨著函數(shù)執(zhí)行完,棧收縮就回收了) freedefer(d) // 執(zhí)行延遲回調(diào)函數(shù) jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) }
代碼說明:
- 遍歷 defer 鏈表,一個個執(zhí)行,順序鏈表從前往后執(zhí)行,執(zhí)行一個摘除一個,直到鏈表為空;
- jmpdefer 負(fù)責(zé)跳轉(zhuǎn)到延遲回調(diào)函數(shù)執(zhí)行指令,執(zhí)行結(jié)束之后,跳轉(zhuǎn)回 deferreturn 里執(zhí)行;
- _defer.sp 的值可以用來判斷哪些是當(dāng)前 caller 函數(shù)注冊的,這樣就能保證只執(zhí)行自己函數(shù)注冊的延遲回調(diào)函數(shù);
例如,a() -> b() -> c() ,a 調(diào)用 b,b 調(diào)用 c ,而 a,b,c 三個函數(shù)都有 defer 注冊延遲函數(shù),那么自然是 c()函數(shù)返回的時候,執(zhí)行 c 的回調(diào);
2. defer 傳遞參數(shù)
1) 預(yù)計算參數(shù)
在前面描述 _defer 數(shù)據(jù)結(jié)構(gòu)的時候說到內(nèi)存結(jié)構(gòu)如下:
_defer 在棧上作為一個 header,延遲回調(diào)函數(shù)( defer )的參數(shù)和返回值緊接著 _defer 放置,而這個參數(shù)值是在 defer 執(zhí)行的時候就設(shè)置好了,也就是預(yù)計算參數(shù),而非等到執(zhí)行 defer 函數(shù)的時候才去獲取。
舉個例子,執(zhí)行 defer func(x, y) 的時候,x,y 這兩個實參是計算的出來的,Go 中的函數(shù)調(diào)用都是值傳遞。那么就會把 x,y 的值拷貝到 _defer 結(jié)構(gòu)體之后。再看個例子:
package main func main() { var x = 1 defer println(x) x += 2 return }
這個程序輸出是什么呢?是 1 ,還是 3 ?答案是:1 。defer 執(zhí)行的函數(shù)是 println ,println 參數(shù)是 x ,x 的值傳進(jìn)去的值則是在 defer 語句執(zhí)行的時候就確認(rèn)了的。
2) defer 的參數(shù)準(zhǔn)備
defer 延遲函數(shù)執(zhí)行的參數(shù)已經(jīng)保存在和 _defer 一起的連續(xù)內(nèi)存塊了。那么執(zhí)行 defer 函數(shù)的時候,參數(shù)是哪里來呢?當(dāng)然不是直接去 _defer 的地址找。因為這里是走的標(biāo)準(zhǔn)的函數(shù)調(diào)用。
在 Go 語言中,一個函數(shù)的參數(shù)由 caller 函數(shù)準(zhǔn)備好,比如說,一個 main() -> A(7) -> B(a) 形成類似以下的棧幀:
所以,deferreturn 除了跳轉(zhuǎn)到 defer 函數(shù)指令,還需要做一個事情:把 defer 延遲回調(diào)函數(shù)需要的參數(shù)準(zhǔn)備好(空間和值)。那么就是如下代碼來做的視線:
func deferreturn(arg0 uintptr) { 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)) } }
arg0 就是 caller 用來放置 defer 參數(shù)和返回值的棧地址。這段代碼的意思就是,把 _defer 預(yù)先的準(zhǔn)備好的參數(shù),copy 到 caller 棧幀的某個地址(arg0)。
3. 執(zhí)行多條 defer
前面已經(jīng)詳細(xì)說明了,_defer 是一個鏈表,表頭是 goroutine._defer 結(jié)構(gòu)。一個協(xié)程的函數(shù)注冊的是掛同一個鏈表,執(zhí)行的時候按照 rsp 來區(qū)分函數(shù)。并且,這個鏈表是把新元素插在表頭,而執(zhí)行的時候是從前往后執(zhí)行,所以這里導(dǎo)致了一個 LIFO 的特性,也就是先注冊的 defer 函數(shù)后執(zhí)行。
4. defer 和 return 運行順序
包含 defer 語句的函數(shù)返回時,先設(shè)置返回值還是先執(zhí)行 defer 函數(shù)?
1) 函數(shù)的調(diào)用過程
要理解這個過程,首先要知道函數(shù)調(diào)用的過程:
- go 的一行函數(shù)調(diào)用語句其實非原子操作,對應(yīng)多行匯編指令,包括 1)參數(shù)設(shè)置,2) call 指令執(zhí)行;
- 其中 call 匯編指令的內(nèi)容也有兩個:返回地址壓棧(會導(dǎo)致 rsp 值往下增長,rsp-0x8),callee 函數(shù)地址加載到 pc 寄存器;
- go 的一行函數(shù)返回 return語句其實也非原子操作,對應(yīng)多行匯編指令,包括 1)返回值設(shè)置 和 2)ret 指令執(zhí)行;
- 其中 ret 匯編指令的內(nèi)容是兩個,指令 pc 寄存器恢復(fù)為 rsp 棧頂保存的地址,rsp 往上縮減,rsp+0x8;
- 參數(shù)設(shè)置在 caller 函數(shù)里,返回值設(shè)置在 callee 函數(shù)里;
- rsp, rbp 兩個寄存器是棧幀的最重要的兩個寄存器,這兩個值劃定了棧幀;
最重要的一點:Go 的 return 的語句調(diào)用是個復(fù)合操作,可以對應(yīng)一下兩個操作序列:
- 設(shè)置返回值
- ret 指令跳轉(zhuǎn)到 caller 函數(shù)
2) return 之后是先返回值還是先執(zhí)行 defer 函數(shù)?
Golang 官方文檔有明確說明:
That is, if the surrounding function returns through an explicit return statement, deferred functions are executedafter any result parameters are set by that return statementbutbefore the function returns to its caller.
也就是說,defer 的函數(shù)鏈調(diào)用是在設(shè)置了返回值之后,但是在運行指令上下文返回到 caller 函數(shù)之前。
所以含有 defer 注冊的函數(shù),執(zhí)行 return 語句之后,對應(yīng)執(zhí)行三個操作序列:
- 設(shè)置返回值
- 執(zhí)行 defer 鏈表
- ret 指令跳轉(zhuǎn)到 caller 函數(shù)
那么,根據(jù)這個原理我們來解析如下的行為:
func f1 () (r int) { t := 1 defer func() { t = t + 5 }() return t } func f2() (r int) { defer func(r int) { r = r + 5 }(r) return 1 } func f3() (r int) { defer func () { r = r + 5 } () return 1 }
這三個函數(shù)的返回值分別是多少?
答案:f1() -> 1,f2() -> 1,f3() -> 6 。
a) 函數(shù) f1 執(zhí)行 return t 語句之后:
- 設(shè)置返回值 r = t,這個時候局部變量 t 的值等于 1,所以 r = 1;
- 執(zhí)行 defer 函數(shù),t = t+5 ,之后局部變量 t 的值為 6;
- 執(zhí)行匯編 ret 指令,跳轉(zhuǎn)到 caller 函數(shù);
所以,f1() 的返回值是 1 ;
b) 函數(shù) f2 執(zhí)行 return 1 語句之后:
- 設(shè)置返回值 r = t,這個時候局部變量 t 的值等于 1,所以 r = 1;
- 執(zhí)行 defer 函數(shù),t = t+5 ,之后局部變量 t 的值為 6;
- 執(zhí)行匯編 ret 指令,跳轉(zhuǎn)到 caller 函數(shù);
所以,f2() 的返回值還是 1 ;
c) 函數(shù) f3 執(zhí)行 return 1 語句之后:
- 設(shè)置返回值 r = 1;
- 執(zhí)行 defer 函數(shù),r = r+5 ,之后返回值變量 r 的值為 6(這是個閉包函數(shù),注意和 f2 區(qū)分);
- 執(zhí)行匯編 ret 指令,跳轉(zhuǎn)到 caller 函數(shù);
所以,f1() 的返回值是 6 。
- defer 關(guān)鍵字執(zhí)行對應(yīng) _defer 數(shù)據(jù)結(jié)構(gòu),在 go1.1 - go1.12 期間一直是堆上分配,在 go1.13 之后優(yōu)化成棧上分配 _defer 結(jié)構(gòu),性能提升明顯;
- _defer 大部分場景是分配在棧上的,但是遇到循環(huán)嵌套的場景會分配到堆上,所以編程時要注意 defer 使用場景,否則可能出性能問題;
- _defer 對應(yīng)一個注冊的延遲回調(diào)函數(shù)(defer),defer 函數(shù)的參數(shù)和返回值緊跟 _defer,可以理解成 header,_defer 和函數(shù)參數(shù),返回值所在內(nèi)存是一塊連續(xù)的空間,其中 _defer.siz 指明參數(shù)和返回值的所占空間大小;
- 同一個協(xié)程里 defer 注冊的函數(shù),都掛在一個鏈表中,表頭為 goroutine._defer;
新元素插入在最前面,遍歷執(zhí)行的時候則是從前往后執(zhí)行。所以 defer 注冊函數(shù)具有 LIFO 的特性,也就是后注冊的先執(zhí)行;
不同的函數(shù)都在這個鏈表上,以 _defer.sp 區(qū)分;
defer 的參數(shù)是預(yù)計算的,也就是在 defer 關(guān)鍵字執(zhí)行的時候,參數(shù)就確認(rèn),賦值在 _defer 的內(nèi)存塊后面。執(zhí)行的時候,copy 到棧幀對應(yīng)的位置上;
return 對應(yīng) 3 個動作的復(fù)合操作:設(shè)置返回值、執(zhí)行 defer 函數(shù)鏈表、ret 指令跳轉(zhuǎn)。
參考:編程寶庫 go 語言教程。
到此這篇關(guān)于Go defer 原理和源碼剖析的文章就介紹到這了,更多相關(guān)Go defer 原理 內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang結(jié)合ip2region實現(xiàn)ip歸屬地查詢
ip2region - 是一個離線IP地址定位庫和IP定位數(shù)據(jù)管理框架,提供了眾多主流編程語言的 xdb 數(shù)據(jù)生成和查詢客戶端實現(xiàn),下面我們就來看看Golang如何結(jié)合ip2region實現(xiàn)ip歸屬地查詢吧2024-03-03Go構(gòu)建器模式構(gòu)建復(fù)雜對象方法實例
本文介紹了構(gòu)建器模式,如何通過構(gòu)建器對象構(gòu)建復(fù)雜業(yè)務(wù)對象的方法實例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-12-12Go語言基礎(chǔ)Json序列化反序列化及文件讀寫示例詳解
這篇文章主要為大家介紹了Go語言基礎(chǔ)Json序列化反序列化以及文件讀寫的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2021-11-11Go 循環(huán)結(jié)構(gòu)for循環(huán)使用教程全面講解
這篇文章主要為大家介紹了Go 循環(huán)結(jié)構(gòu)for循環(huán)使用全面講解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-10-10