深刻理解defer(上)defer基礎

深刻理解 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 的實現機制。

相關文章
相關標籤/搜索