Go 高階指南 defer 實現原理

defer 語句用於延遲函數的調用,使用 defer 關鍵字修飾一個函數,會將這個函數壓入棧中,當函數返回時,再把棧中函數取出執行。

老規矩,我們先來答幾道題試試水。

答題環節

  1. 下面程序輸出什麼?
func deferTest() {
  var a = 1
  defer fmt.Println(a)
  
  a = 2
  return
}



是:1

解析:延遲函數 fmt.Println(a) 的參數在 defer 語句出現的時候就已經確定下來了,所以不管後面如何修改 a 變量,都不會影響延遲函數。

  1. 下面程序輸出什麼?
package main

import "fmt"

func main() {
 deferTest()
}

func deferTest() {
 var arr = [3]int{1, 2, 3}
 defer printTest(&arr)
 
 arr[0] = 4
 return
}

func printTest(array *[3]int) {
 for i := range array {
  fmt.Println(array[i])
 }
}



是:

10
2
3

解析:延遲函數 printTest() 的參數在 defer 語句出現的時候就已經確定下來了,即爲數組的地址,延遲函數執行的時機是在 return 語句之前,所以對數組的最終修改的值會被打印出來。

  1. 下面程序輸出什麼?
package main

import "fmt"

func main() {
 res := deferTest()
 fmt.Println(res)
}

func deferTest () (result int) {
  i := 1
  
  defer func() {
    result++
  }()
  
  return i
}



是:

2

解析:函數的 return 語句並不是原子級的,實際的執行過程爲爲設置返回值—>ret,defer 語句是在返回前執行,所以返回過程是:「設置返回值—> 執行 defer—>ret」。所以 return 語句先把 result 設置成 i 的值(1),defer 語句中又把 result 遞增 1 ,所以最終返回值爲 2 。

defer 規則

函數返回過程

上面題目中我們已經瞭解到,函數的 return 語句並不是原子級的,實際上 return 語句只代理彙編指令 ret。返回過程是:「設置返回值—> 執行 defer—>ret」

func deferTest () (result int) {
  i := 1
  
  defer func() {
    result++
  }()
  
  return i
}

上面有 defer 例子的 return 語句實際執行過程是:

result = i
result++
return

主函數擁有匿名返回值,返回字面值時

當主函數有一個匿名返回值,返回時使用字面值,例如返回 “1”,“2”,“3” 這樣的值,此時 defer 語句是不能操作返回值的。

func test() int {
  var i int
  defer func() {
    i++
  }()
  
  return 1
}

上面的 return 語句,直接把1作爲返回值,延遲函數無法操作返回值,所以也就不能修改返回值。

主函數擁有匿名返回值,返回變量時

當主函數有一個匿名返回值,返回會使用本地或者全局變量,此時 defer 語句可以引用到返回值,但不會改變返回值。

func test() int {
  var i int
  defer func() {
    i++
  }()
  
  return i
}

上面的函數,返回一個局部變量,defer 函數也有操作這個局部變量。對於匿名返回值來說,我們可以假定仍然有一個變量用來存儲返回值,例如假定返回值變量爲 ”aaa”,上面的返回語句可以拆分成以下過程:

aaa = i
i++
return

由於 i 是整型,會將值拷貝給變量 aaa,所以 defer 語句中修改 i 的值,對函數返回值不造成影響。

主函數擁有具名返回值時

主函聲明語句中帶名字的返回值,會被初始化成一個局部變量,函數內部可以像使用局部變量一樣使用該返回值。如果 defer 語句操作該返回值,可能會改變返回結果。

package main

import "fmt"

func main() {
 res := test()
 fmt.Println(res) // 1
}
func test() (i int) {
 defer func() {
  i++
 }()
 return 0
}

上面的返回語句可以拆分成以下過程:

i = 0
i++
return

defer 實現原理

源碼包 src/src/runtime/runtime2.go:_defer 定義了 defer 的數據結構:

type _defer struct {
 siz     int32 // includes both arguments and results
 started bool
 heap    bool
 // openDefer indicates that this _defer is for a frame with open-coded
 // defers. We have only one defer record for the entire frame (which may
 // currently have 0, 1, or more defers active).
 openDefer bool
 sp        uintptr  // sp at time of defer
 pc        uintptr  // pc at time of defer
 fn        *funcval // can be nil for open-coded defers
 _panic    *_panic  // panic that is running defer
 link      *_defer

 // If openDefer is true, the fields below record values about the stack
 // frame and associated function that has the open-coded defer(s). sp
 // above will be the sp for the frame, and pc will be address of the
 // deferreturn call in the function.
 fd   unsafe.Pointer // funcdata for the function associated with the frame
 varp uintptr        // value of varp for the stack frame
 // framepc is the current pc associated with the stack frame. Together,
 // with sp above (which is the sp associated with the stack frame),
 // framepc/sp can be used as pc/sp pair to continue a stack trace via
 // gentraceback().
 framepc uintptr
}

defer 語句後面是要跟一個函數的,所以 defer 的數據結構跟一般的函數類似,不同之處是 defer 結構含有一個指針,用於指向另一個 defer ,每個 goroutine 數據結構中實際上也有一個 defer 指針指向一個 defer 的單鏈表,每次聲明一個 defer 時就將 defer 插入單鏈表的表頭,每次執行 defer 時就從單鏈表的表頭取出一個 defer 執行。保證 defer 是按 FIFO 方式執行的。

defer 的創建和執行

源碼包 src/runtime/panic.go 中定義了兩個方法分別用於創建 defer 和執行 defer。

歸納總結

  1. defer 定義的延遲函數的參數在 defer 語句出時就已經確定下來了

  2. defer 定義順序與實際執行順序相反

  3. return 不是原子級操作的,執行過程是: 保存返回值—> 執行 defer —> 執行 ret

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/JZCtt8yWd9PKIQFtMa1iNw