深入理解defer(上)defer基礎

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

posted @ 2019-06-19 08:48 愛寫程序的阿波張 閱讀(...) 評論(...) 編輯 收藏
内部期期公开一波中特