Go 高階指南 defer 實現原理
defer 語句用於延遲函數的調用,使用 defer 關鍵字修飾一個函數,會將這個函數壓入棧中,當函數返回時,再把棧中函數取出執行。
老規矩,我們先來答幾道題試試水。
答題環節
- 下面程序輸出什麼?
func deferTest() {
var a = 1
defer fmt.Println(a)
a = 2
return
}
答
案
是:1
解析:延遲函數 fmt.Println(a) 的參數在 defer 語句出現的時候就已經確定下來了,所以不管後面如何修改 a 變量,都不會影響延遲函數。
- 下面程序輸出什麼?
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 語句之前,所以對數組的最終修改的值會被打印出來。
- 下面程序輸出什麼?
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 規則
-
延遲函數的參數在 defer 語句出現時就已經確定
-
注意:對於指針類型的參數,規則仍然適用,不過延遲函數的參數是一個地址值,這種情況下,defer 後面的語句對變量的修改可能會影響延遲函數。
-
延遲函數執行按 「先進後出」 順序執行,即先出現的 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
}
-
sp 函數棧指針
-
pc 程序計數器
-
fn 函數地址
-
link 指向自身結構的指針,用於鏈接多個 defer
defer 語句後面是要跟一個函數的,所以 defer 的數據結構跟一般的函數類似,不同之處是 defer 結構含有一個指針,用於指向另一個 defer ,每個 goroutine 數據結構中實際上也有一個 defer 指針指向一個 defer 的單鏈表,每次聲明一個 defer 時就將 defer 插入單鏈表的表頭,每次執行 defer 時就從單鏈表的表頭取出一個 defer 執行。保證 defer 是按 FIFO 方式執行的。
defer 的創建和執行
源碼包 src/runtime/panic.go 中定義了兩個方法分別用於創建 defer 和執行 defer。
-
deferproc():在聲明 defer 處調用,其將 defer 函數存入 goroutine 的鏈表中;
-
deferreturn():在 return 指令,準確的講是在 ret 指令前調用,其將 defer 從 goroutine 鏈表中取出並執行。
歸納總結
-
defer 定義的延遲函數的參數在 defer 語句出時就已經確定下來了
-
defer 定義順序與實際執行順序相反
-
return 不是原子級操作的,執行過程是: 保存返回值—> 執行 defer —> 執行 ret
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/JZCtt8yWd9PKIQFtMa1iNw