? 個人博客:https://blog.csdn.net/Newin2020?type=blog
📝 專欄地址:https://blog.csdn.net/newin2020/category_12898955.html
📣 專欄定位:為 0 基礎剛入門 Golang 的小伙伴提供詳細的講解,也歡迎大佬們一起交流~
📚 專欄簡介:在這個專欄,我將帶著大家從 0 開始入門 Golang 的學習。在這個 Golang 的新人系列專欄下,將會總結 Golang 入門基礎的一些知識點,并由淺入深的學習這些知識點,方便大家快速入門學習~
?? 如果有收獲的話,歡迎點贊 👍 收藏 📁 關注,您的支持就是我創作的最大動力 💪
1. 快速了解
defer 后面的代碼會在函數 return 后執行,并且執行的順序是與代碼的順序相反,即倒序執行。
//main 2 1
func main() {defer fmt.Println("1")defer fmt.Println("2")fmt.Println("main")return
}
使用 defer 需要注意其執行的時機,以免造成意料之外的影響,例如它可能會修改返回值:
func deferReturn() (ret int) {defer func() {ret++}()return 10
}func main() {ret := deferReturn()fmt.Printf("ret = %d\r\n",ret) //11
}
2. defer 執行邏輯
我們先來看一段簡潔的代碼。
func A() {defer B()//code to do something
}
上面這段代碼,編譯后的偽指令是下面這樣的。defer 指令對應到兩部分內容,其中 deferproc 負責把要執行的函數信息保存起來,我們稱之為 defer 注冊。而 deferproc 函數會返回 0,下面 if 分支和 panic recover 有關,可以先忽略不看,同時對應要跳轉的 ret 這里也先忽略不看。
func A() {r = deferproc(8, B)if r > 0 {goto ret}//code to do somethingruntime.deferreturn()return
ret:runtime.deferreturn()
}
去掉忽略的部分,程序的整體邏輯就比較清晰了。在 defer 注冊完成后,程序就會執行后面的邏輯,直到返回之前通過 deferreturn 執行注冊的 defer 函數,即 defer 調用。正是因為先注冊后調用,才實現了 defer 延遲執行的效果。
func A() {r = deferproc(8, B) // 1.注冊//code to do somethingruntime.deferreturn() // 2.調用return
}
看回 defer 注冊部分,defer 注冊的信息會注冊到一個鏈表,而當前執行的 goroutine 會持有這個鏈表的頭指針。每個 goroutine 在運行時都有一個對應的結構體 g,其中有一個字段就指向 defer 鏈表頭。
defer 鏈表鏈起來的是一個一個 _defer 結構體,新注冊的 defer 會添加到鏈表頭,執行時也是從頭開始,這也就是 defer 會表現為倒序執行的原因。
在展開 _defer 結構之前,先看一個例子,這里函數 A 注冊了一個 defer 函數 A1。
func A1(a int) {fmt.Println(a)
}
func A() {a, b := 1, 2defer A1(a)a = a + bfmt.Println(a, b)
}
我們來看看函數調用棧,A 的棧幀首先會是存放兩個局部變量。接著 A1 只有一個參數,因此局部變量下面存放參數 a 的值 1,然后就要注冊 defer 函數 A1 了。
deferproc 函數原型只有兩個參數,第一個參數是 defer 函數 A1 的參數加返回值共占多大空間。這里 A1 沒有返回值,只需要一個整形參數和一個指針變量,因此 64 位下要占 4 字節。
func deferproc (siz int32, fn *funcval)
第二個參數是一個 function value,前面函數部分我們也介紹過,沒有捕獲列表的 function value 在編譯階段就會做出優化,即在只讀數據段分配一個共用的 funcval 結構體,結構體中的指針會指向函數 A1 指令入口,所以 deferproc 的第二個參數就是結構體的地址 addr2。
func deferproc (siz = 4, fn = addr2)
至此我們先把 _defer 的結構體展開了看一下:
type _defer struct {siz int32 // 參數和返回值共占多少字節,這段空間會直接分配在_defer結構體后面,用于在注冊時保存參數,并在執行時拷貝到調用者參數與返回值空間started bool // 標記defer是否已經執行sp uintptr // 記錄注冊這個defer的函數棧指針(調用者棧指針),函數可以通過它判斷自己注冊的defer是否已經執行完了pc uintptr // deferproc的返回地址fn *funcval // 注冊的function value函數_panic *_paniclink *_defer // 鏈接到前一個注冊的defer結構體
}
當 deferproc 函數調用時,編譯器會在后面繼續開辟一段空間,用于存放 defer 函數的返回值和參數,由于在這個例子里沒有返回值,因此只分配 defer 函數的一個參數的空間,這一段空間會被直接拷貝到 _defer 結構體的后面。
另外,返回值地址和調用者函數的 BP 則放在 deferproc 兩個參數之后。
在 deferproc 函數執行時,需要堆分配一段空間用于存放 _defer 結構體,而在 _defer 結構體后面也會分配一段空間用于存放 siz 大小的參數與返回值,這里由于沒有返回值因此存放參數 a。(注意這里所有的變量存放的順序是從下至上的,因此參數 a 雖然說是存放在 _defer 結構體的后面,但其實分配的空間在該結構體存放的位置之上)
然后這個 _defer 結構體就會被添加到 defer 鏈表頭,至此 deferproc 注冊結束。
_defer 結構體預分配
實際上 go 語言會預分配不同規格的 defer 池,執行時從空閑的 _defer 中取一個出來用即可。如果沒有空閑的或者沒有大小合適的,則會再進行堆分配,用完以后再放回空閑的 _defer 池,這樣就可以避免頻繁地堆分配與回收。
讓我們再回到函數代碼的執行,當代碼執行到函數 A 中的 a = a + b 這行代碼時,變量 a 被賦值為 3,然后下一步會輸出局部變量 a 和 b 的值,即 3 和 2。
接下來就到 deferreturn 執行 defer 鏈表了,此時會從當前 goroutine 拿到鏈表頭上的這個 _defer 結構體,通過 _defer 結構體里的 fn = addr2 找到對應的 funcval,然后通過 funcval 中的 fn 可以拿到函數入口的地址 addr1。
在調用 A1 時,會把 _defer 后面的參數與返回值整個拷貝到 A1 的調用者棧上,然后 A1 開始執行,此時就會輸出 1。
這里的關鍵是 defer 函數的參數在注冊時拷貝到堆上,執行時又拷貝到棧上。并不會去使用到 A 函數棧中保存的局部變量 a 的值 3,所以即使在 defer 函數注冊后修改了這個局部變量 a 的值,也不會影響到執行 defer 函數時用到的變量 a。
既然 deferproc 注冊的是一個 function value,我們下面就來看看捕獲列表時是什么情況,變量 a 在 defer 函數注冊后進行修改是否能影響到 defer 函數里使用的變量。
3. defer + 閉包
在下面這個例子中,defer 函數不止要傳遞局部變量 b 做參數,還捕獲了外層函數的局部變量 a 并形成了閉包。
func A() {a, b := 1, 2defer func(b int) {a = a + bfmt.Println(a, b)}(b)a = a + bfmt.Println(a, b)
}
匿名函數會由編譯器按照 A_func1 這樣的形式命名。如下圖所示,假設這個閉包函數的指令入口地址為 addr1。
由于捕獲變量 a 除了初始化賦值外還被修改過,所以局部變量 a 改為堆分配,而棧上存儲它的地址。另外,還有一個局部變量 b 也要分配。
然后創建閉包對象,堆分配一個 funcval 結構體,并且捕獲列表中存儲 a 的地址。
deferproc 執行時,_defer 結構體中的 fn 就是這個 funcval 結構體的起始地址。除此之外,還要拷貝參數 b 的值到 _defer 結構體的后面,然后把這個 _defer 結構體添加到 defer 鏈表頭。
至此,deferproc 注冊結束。然后接著執行到 a = a + b 這行代碼,變量 a 被賦值為 3。而下一步就自然輸出 a 和 b 的變量值,即 3 和 2。
接著就到 deferreturn 了,從 defer 鏈表頭拿到這個 defer 結構體,執行注冊的 defer 函數時,需要把參數 b 拷貝到棧上的參數空間。
另外,閉包函數也會通過寄存器存儲的 funcval 地址加上偏移,找到捕獲變量 a 的地址。
當執行到 defer 函數 A_func1 里的 a = a + b 這行代碼時,此時的 a = 3 且 b = 2,所以 a 會被賦值為 5。因此,下一步將會輸出變量 a 和 b 的值,即 5 和 2。
可以發現當變量 a 變成被捕獲的變量形成閉包后,在注冊完 defer 函數后修改變量 a 是可以影響到 defer 函數中使用的變量值的。這是因為此時的變量 a 發生了逃逸,不再分配到棧上而是分配到堆上,defer 函數的變量 a 最終將會從堆上獲取具體的值。