深入理解defer(上)defer基礎

愛寫程式的阿波張發表於2019-06-19

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

相關文章