深入理解 defer 分上下兩篇文章,本文為上篇,主要介紹如下內容:
-
為什么需要 defer;
-
defer 語法及語義;
-
defer 使用要點;
-
defer 語句中的函數到底是在 return 語句之后被調用還是 return 語句之前被調用。
為什么需要 defer
先來看一段沒有使用 defer 的代碼:
func f() {
r := getResource() //0,獲取資源
......
if ... {
r.release() //1,釋放資源
return
}
......
if ... {
r.release() //2,釋放資源
return
}
......
if ... {
r.release() //3,釋放資源
return
}
......
r.release() //4,釋放資源
return
}
f() 函數首先通過調用 getResource() 獲取了某種資源(比如打開文件,加鎖等),然后進行了一些我們不太關心的操作,但這些操作可能會導致 f() 函數提前返回,為了避免資源泄露,所以每個 return 之前都調用了 r.release() 函數對資源進行釋放。這段代碼看起來並不糟糕,但有兩個小問題:代碼臃腫和可維護性比較差。臃腫倒是其次,主要問題在於代碼的可維護性差,因為隨着開發和維護的進行,修改代碼在所難免,一旦對 f() 函數進行修改添加某個提前返回的分支,就很有可能在提前 return 時忘記調用 r.release() 釋放資源,從而導致資源泄漏。
那么我們如何改善上述兩個問題呢?一個不錯的方案就是通過 defer 調用 r.release() 來釋放資源:
func f() {
r := getResource() //0,獲取資源
defer r.release() //1,注冊延遲調用函數,f()函數返回時才會調用r.release函數釋放資源
......
if ... {
return
}
......
if ... {
return
}
......
if ... {
return
}
......
return
}
可以看到通過使用 defer 調用 r.release(),我們不需要在每個 return 之前都去手動調用 r.release() 函數,代碼確實精簡了一點,重要的是不管以后加多少提前 return 的代碼,都不會出現資源泄露的問題,因為不管在什么地方 return ,r.release() 函數始終都會被調用。
defer 語法及語義
defer語法很簡單,直接在普通寫法的函數調用之前加 defer 關鍵字即可:
defer xxx(arg0, arg1, arg2, ......)
defer 表示對緊跟其后的 xxx() 函數延遲到 defer 語句所在的當前函數返回時再進行調用。比如前文代碼中注釋 1 處的 defer r.release() 表示等 f() 函數返回時再調用 r.release() 。下文我們稱 defer 語句中的函數叫 defer函數。
defer 使用要點
對 defer 的使用需要注意如下幾個要點:
-
延遲對函數進行調用;
-
即時對函數的參數進行求值;
-
根據 defer 順序反序調用;
下面我們用例子來簡單的看一下這幾個要點。
defer 函數延遲調用
func f() {
defer fmt.Println("defer")
fmt.Println("begin")
fmt.Println("end")
return
}
這段代碼首先會輸出 begin 字符串,然后是 end ,最后才輸出 defer 字符串。
defer 函數參數即時求值
func g(i int) {
fmt.Println("g i:", i)
}
func f() {
i := 100
defer g(i) //1
fmt.Println("begin i:", i)
i = 200
fmt.Println("end i:", i)
return
}
這段代碼首先輸出 begin i: 100,然后輸出 end i: 200,最后輸出 g i: 100 ,可以看到 g() 函數雖然在f函數返回時才被調用,但傳遞給 g() 函數的參數還是100,因為代碼 1 處的 defer g(i) 這條語句執行時 i 的值是100。也就是說 defer 函數會被延遲調用,但傳遞給 defer 函數的參數會在 defer 語句處就被准備好。
反序調用
func f() {
defer fmt.Println("defer01")
fmt.Println("begin")
defer fmt.Println("defer02")
fmt.Println("----")
defer fmt.Println("defer03")
fmt.Println("end")
return
}
這段程序的輸出如下:
begin
----
end
defer03
defer02
defer01
可以看出f函數返回時,第一個 defer 函數最后被執行,而最后一個 defer 函數卻第一個被執行。
defer 函數的執行與 return 語句之間的關系
到目前為止,defer 看起來都還比較好理解。下面我們開始把問題復雜化
package main
import "fmt"
var g = 100
func f() (r int) {
defer func() {
g = 200
}()
fmt.Printf("f: g = %d\n", g)
return g
}
func main() {
i := f()
fmt.Printf("main: i = %d, g = %d\n", i, g)
}
輸出:
$ ./defer
f: g =100
main: i =100, g =200
這個輸出還是比較容易理解,f() 函數在執行 return g 之前 g 的值還是100,所以 main() 函數獲得的 f() 函數的返回值是100,因為 g 已經被 defer 函數修改成了200,所以在 main 中輸出的 g 的值為200,看起來 defer 函數在 return g 之后才運行。下面稍微修改一下上面的程序:
package main
import "fmt"
var g = 100
func f() (r int) {
r = g
defer func() {
r = 200
}()
fmt.Printf("f: r = %d\n", r)
r = 0
return r
}
func main() {
i := f()
fmt.Printf("main: i = %d, g = %d\n", i, g)
}
輸出:
$ ./defer
f: r =100
main: i =200, g =100
從這個輸出可以看出,defer 函數修改了 f() 函數的返回值,從這里看起來 defer 函數的執行發生在 return r 之前,然而上一個例子我們得出的結論是 defer 函數在 return 語句之后才被調用執行,這兩個結論很矛盾,到底是怎么回事呢?
僅僅從go語言的角度來說確實不太好理解,我們需要深入到匯編來分析一下。
老套路,使用 gdb 反匯編一下 f() 函數:
0x0000000000488a30<+0>: mov %fs:0xfffffffffffffff8,%rcx 0x0000000000488a39<+9>: cmp 0x10(%rcx),%rsp 0x0000000000488a3d<+13>: jbe 0x488b33 <main.f+259> 0x0000000000488a43<+19>: sub $0x68,%rsp 0x0000000000488a47<+23>: mov %rbp,0x60(%rsp) 0x0000000000488a4c<+28>: lea 0x60(%rsp),%rbp 0x0000000000488a51<+33>: movq $0x0,0x70(%rsp) # 初始化返回值r為0 0x0000000000488a5a<+42>: mov 0xbd66f(%rip),%rax # 0x5460d0 <main.g> 0x0000000000488a61<+49>: mov %rax,0x70(%rsp) # r = g 0x0000000000488a66<+54>: movl $0x8,(%rsp) 0x0000000000488a6d<+61>: lea 0x384a4(%rip),%rax # 0x4c0f18 0x0000000000488a74<+68>: mov %rax,0x8(%rsp) 0x0000000000488a79<+73>: lea 0x70(%rsp),%rax 0x0000000000488a7e<+78>: mov %rax,0x10(%rsp) 0x0000000000488a83<+83>: callq 0x426c00 <runtime.deferproc> 0x0000000000488a88<+88>: test %eax,%eax 0x0000000000488a8a<+90>: jne 0x488b23 <main.f+243> 0x0000000000488a90<+96>: mov 0x70(%rsp),%rax 0x0000000000488a95<+101>: mov %rax,(%rsp) 0x0000000000488a99<+105>: callq 0x408950 <runtime.convT64> 0x0000000000488a9e<+110>: mov 0x8(%rsp),%rax 0x0000000000488aa3<+115>: xorps %xmm0,%xmm0 0x0000000000488aa6<+118>: movups %xmm0,0x50(%rsp) 0x0000000000488aab<+123>: lea 0x101ee(%rip),%rcx # 0x498ca0 0x0000000000488ab2<+130>: mov %rcx,0x50(%rsp) 0x0000000000488ab7<+135>: mov %rax,0x58(%rsp) 0x0000000000488abc<+140>: nop 0x0000000000488abd<+141>: mov 0xd0d2c(%rip),%rax# 0x5597f0 <os.Stdout> 0x0000000000488ac4<+148>: lea 0x495f5(%rip),%rcx# 0x4d20c0 <go.itab.*os.File,io.Writer> 0x0000000000488acb<+155>: mov %rcx,(%rsp) 0x0000000000488acf<+159>: mov %rax,0x8(%rsp) 0x0000000000488ad4<+164>: lea 0x31ddb(%rip),%rax # 0x4ba8b6 0x0000000000488adb<+171>: mov %rax,0x10(%rsp) 0x0000000000488ae0<+176>: movq $0xa,0x18(%rsp) 0x0000000000488ae9<+185>: lea 0x50(%rsp),%rax 0x0000000000488aee<+190>: mov %rax,0x20(%rsp) 0x0000000000488af3<+195>: movq $0x1,0x28(%rsp) 0x0000000000488afc<+204>: movq $0x1,0x30(%rsp) 0x0000000000488b05<+213>: callq 0x480b20 <fmt.Fprintf> 0x0000000000488b0a<+218>: movq $0x0,0x70(%rsp) # r = 0 # ---- 下面5條指令對應着go代碼中的 return r 0x0000000000488b13<+227>: nop 0x0000000000488b14<+228>: callq 0x427490 <runtime.deferreturn> 0x0000000000488b19<+233>: mov 0x60(%rsp),%rbp 0x0000000000488b1e<+238>: add $0x68,%rsp 0x0000000000488b22<+242>: retq # --------------------------- 0x0000000000488b23<+243>: nop 0x0000000000488b24<+244>: callq 0x427490 <runtime.deferreturn> 0x0000000000488b29<+249>: mov 0x60(%rsp),%rbp 0x0000000000488b2e<+254>: add $0x68,%rsp 0x0000000000488b32<+258>: retq 0x0000000000488b33<+259>: callq 0x44f300 <runtime.morestack_noctxt> 0x0000000000488b38<+264>: jmpq 0x488a30 <main.f>
f() 函數本來很簡單,但里面使用了閉包和 Printf,所以匯編代碼看起來比較復雜,這里我們只挑重點出來說。f() 函數最后 2 條語句被編譯器翻譯成了如下6條匯編指令:
0x0000000000488b0a<+218>: movq $0x0,0x70(%rsp) # r = 0 # ---- 下面5條指令對應着go代碼中的 return r 0x0000000000488b13<+227>: nop 0x0000000000488b14<+228>: callq 0x427490 <runtime.deferreturn> # deferreturn會調用defer注冊的函數 0x0000000000488b19<+233>: mov 0x60(%rsp),%rbp # 調整棧 0x0000000000488b1e<+238>: add $0x68,%rsp # 調整棧 0x0000000000488b22<+242>: retq # 從f()函數返回 # ---------------------------
這6條指令中的第一條指令對應到的go語句是 r = 0,因為 r = 0 之后的下一行語句是 return r ,所以這條指令相當於把 f() 函數的返回值保存到了棧上,然后第三條指令調用了 runtime.deferreturn 函數,該函數會去調用我們在 f() 函數開始處使用 defer 注冊的函數修改 r 的值為200,所以我們在main函數拿到的返回值是200,后面三條指令完成函數調用棧的調整及返回。
從這幾條指令可以得出,准確的說,defer 函數的執行既不是在 return 之后也不是在 return 之前,而是一條go語言的 return 語句包含了對 defer 函數的調用,即 return 會被翻譯成如下幾條偽指令
保存返回值到棧上
調用defer函數
調整函數棧
retq指令返回
到此我們已經知道,前面說的矛盾其實並非矛盾,只是從Go語言層面來理解不好理解而已,一旦我們深入到匯編層面,一切都會顯得那么自然,正所謂匯編之下了無秘密。
總結
-
defer 主要用於簡化編程(以及實現 panic/recover ,后面會專門寫一篇相關文章來介紹)
-
defer 實現了函數的延遲調用;
-
defer 使用要點:延遲調用,即時求值和反序調用;
-
go 語言的 return 會被編譯器翻譯成多條指令,其中包括保存返回值,調用defer注冊的函數以及實現函數返回。
本文我們主要從使用的角度介紹了defer 的基礎知識,下一篇文章我們將會深入 runtime.deferproc 和 runtime.deferreturn 這兩個函數分析 defer 的實現機制。
