深刻理解 defer 分上下兩篇文章,本文爲上篇,主要介紹以下內容:編程
爲何須要 defer;閉包
defer 語法及語義;函數
defer 使用要點;spa
defer 語句中的函數究竟是在 return 語句以後被調用仍是 return 語句以前被調用。翻譯
爲何須要 defer3d
先來看一段沒有使用 defer 的代碼:rest
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() 釋放資源,從而致使資源泄漏。code
那麼咱們如何改善上述兩個問題呢?一個不錯的方案就是經過 defer 調用 r.release() 來釋放資源:blog
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() 函數始終都會被調用。ip
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 的實現機制。