❝「第12期」 距離大叔的80期小目標還有68期,擱了一段時間的大叔又回來了~ 今天大叔要跟你們分享基礎知識點是 —— defer坑坑窪窪的使用細節。仍是那句話,基礎抓得狠,不愁沒飯碗,最怕的就是面試官的面試記錄是這樣寫:「該同窗基礎較差,暫不考慮」,扎不扎心,難不難過。因此,建議提倡鼓勵倡導表揚你們跟着大叔一塊兒踏踏實實地打好基礎,一塊兒進步吧!web
❞
文章開始前,想請你們先看一下關於defer用法的兩道題,聽說這是5年的Gopher都會掉進的坑哦:面試
func increaseA() int {
var i int
defer func() {
i++
}()
return i
}
func increaseB() (r int) {
defer func() {
r++
}()
return r
}
func main() {
fmt.Println(increaseA())
fmt.Println(increaseB())
}
複製代碼
老實說,在不運行代碼的前提下,大叔未可以徹底回答上來。若是你的狀況也跟大叔同樣,或者對defer的知識點有點模凌兩可,那麼大叔這篇文章就有意義了,請接着往下看;若是你能正確回答上了(跪爛的膝蓋還收嗎大神),那麼就當作溫故知新吧。緩存
好,在分析上面題目以前,咱們先來了解一下defer的此生前世是什麼。markdown
defer 是 Go 語言提供的一種用於註冊延遲調用的機制,或者你也能夠認爲 defer 是一個「延遲調用函數」。咱們看一下 defer 關鍵字在 Go 語言源代碼中對應的數據結構:數據結構
type _defer struct {
siz int32 // 參數的長度,函數fn的參數長度
started bool // 該 defer 是否已經執行過
openDefer bool // 是否開發編碼
sp uintptr // 函數棧指針寄存器,通常指向當前函數棧的棧頂
pc uintptr // 程序計數器,有時稱爲指令指針(IP),線程利用它來跟蹤下一個要執行的指令。在大多數處理器中,PC 指向的是下一條指令,而不是當前指令
fn *funcval // 指向傳入的函數地址和參數
_panic *_panic // 指向_panic鏈表
link *_defer // 指向_defer鏈表
...
}
複製代碼
能夠看到,runtime._defer 結構體是延遲調用鏈表上的一個元素,全部的結構體都會經過 link 字段串聯成鏈表。閉包
那麼這個鏈表是怎麼構建的呢?實際上,只要獲取到 新的runtime._defer 結構體,它都會被追加到所在 Goroutine _defer鏈表的最前面,即往 _defer鏈表的表頭追加。app
defer 既然被定義爲 「延遲調用」,說明 defer 語句不會當即執行,而是跟上面提到的那樣,程序獲取到新的 defer 結構體時,會先往延遲調用鏈表的表頭追加該defer結構體。函數
那麼何時會執行延遲調用呢?答案是:在函數return前。oop
在函數return前,當前的 Goroutine 會從表頭開始遍歷延遲調用鏈表依次執行每一個defer(也就是說最早被定義的defer語句會被最後執行,沒錯,就是先進後出的執行順序,因此該延遲調用鏈也能夠理解爲是一個棧),咱們來看一下源碼:ui
func deferreturn(arg0 uintptr) {
gp := getg() // 返回當前的goroutine
d := gp._defer // 獲取g上綁定的第一個defer
if d == nil { // 因爲是遞歸調用,這裏是一個循環終止條件,d上已經沒有綁定的defer了
return
}
sp := getcallersp() // 獲取當前調用者的sp
if d.sp != sp {
// 判斷當前調用者棧是否和defer中保存的一致
// 舉個例子,a()中聲明一個defer1,並調用b(),b中也聲明一個defer2
// 而後defer1和defer2都綁定在同一個g上
// 那麼在b()執行return時,只會執行defer2,由於defer2上綁定的纔是b()的sp
return
}
// 判斷是不是經過開發編碼實現
if d.openDefer {
done := runOpenDeferFrame(gp, d)
if !done {
throw("unfinished open-coded defers in deferreturn")
}
gp._defer = d.link
freedefer(d)
return
}
// Moving arguments around.
//
// Everything called after this point must be recursively
// nosplit because the garbage collector won't know the form
// of the arguments until the jmpdefer can flip the PC over to
// fn.
switch d.siz {
case 0:
// Do nothing.
case sys.PtrSize:
*(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
default:
memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
}
fn := d.fn
d.fn = nil
gp._defer = d.link // g中的defer指向下一個defer
freedefer(d) // 進行釋放,歸還到相應的緩衝區或者讓gc回收
// If the defer function pointer is nil, force the seg fault to happen
// here rather than in jmpdefer. gentraceback() throws an error if it is
// called with a callback on an LR architecture and jmpdefer is on the
// stack, because the stack trace can be incorrect in that case - see
// issue #8153).
_ = fn.fn
jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // // 執行defer中綁定的func
}
複製代碼
整個執行流程能夠理解爲:
上面總體介紹了defer 數據結構以及調用原理,接下來咱們繼續看一下 defer 的使用細節。
在使用 defer 時,不可避免地會涉及到變量引用的問題,實際上,defer 語句定義時對外部變量引用的方式有如下兩種:
外部變量做爲函數參數時,在 defer 定義時就把值傳遞給 defer 函數體緩存起來,至關於將變量值複製一份(注意:若是參數是值類型,那麼將複製值,若是參數是指針,那麼將複製指針而不是複製指針指向的值),看個例子:
func main() {
i := 0
// 做爲函數參數
defer func(x int) {
fmt.Printf("複製值:%v\n", x)
}(i)
defer func(x *int) {
fmt.Printf("複製指針:%v\n", *x)
}(&i)
i++
}
複製代碼
運行輸出:
複製指針:1
複製值:0
複製代碼
外部變量做爲閉包引用時,則會在 defer 函數體真正被調用是根據整個上下文肯定當前的值。看例子:
func main() {
i := 0
// 做爲閉包引用
defer func() {
fmt.Println(i) // 打印 1
}()
i++
}
複製代碼
使用 defer 最容易踩坑的地方是和帶命名返回參數的函數一塊兒使用,這種方式又稱爲函數顯式返回。好比這樣:
func test() (r int) {
defer func() {
// todo
}()
return 0
}
複製代碼
避免掉坑的關鍵是要正確理解下面這條語句:
return xxxx
複製代碼
沒錯,就是這個return語句,實際上 return xxxx 並非一個原子指令,通過編譯後,它會變成三條指令(return 「三步曲」):
返回值 = xxxx
調用 defer 函數體
直接 return , 結束當前函數
複製代碼
第1、三步纔是 return 語句的真正命令,第二步是 defer 定義的語句,這裏有可能會操做返回值。
實踐是檢驗真理的惟一標準!結合上面的知識點咱們來盤幾道題吧。
func f1() (r int) {
defer func() {
r++
}()
return 0
}
func main() {
fmt.Println(f1())
}
複製代碼
首先,咱們能夠看到,defer 定義時對外部變量引用的方式是 閉包引用的方式,既然是閉包引用的方式,那麼 defer 函數體內對返回值操做將會影響返回值;接着咱們根據 return 的「三步曲」對上面的代碼進行拆解:
func f1() (r int) {
// 1.賦值
r = 0
// 2.閉包引用,返回值被修改
defer func() {
r++
}()
// 3.直接 return
return
}
func main() {
fmt.Println(f1())
}
複製代碼
通過拆解後,整個操做已經很是清晰了,defer 是閉包引用,返回值被修改,因此函數 f1() 返回 1。
再來:
func f2() (r int) {
t := 1
defer func() {
t = t + 5
}()
return t
}
func main() {
fmt.Println(f2())
}
複製代碼
一樣,先看 defer 定義時對外部變量引用的方式,明顯也是閉包引用;接着根據 return 的「三步曲」對上面的代碼進行拆解:
func f2() (r int) {
t := 1
// 1.賦值
r = t
// 2.閉包引用,可是沒有修改返回值 r,修改的是變量 t
defer func() {
t = t + 5
}()
// 3.直接 return
return
}
func main() {
fmt.Println(f2())
}
複製代碼
經過上面的拆解分析,咱們能夠看到,即便 defer 是閉包引用,可是 defer 函數體內修改的是變量 t,跟返回值 r 沒有一毛錢關係,因此函數 f2() 返回 1。
趁熱打鐵,繼續搞:
func f3() (r int) {
defer func(r int) {
r = r + 5
}(r)
return 1
}
func main() {
fmt.Println(f3())
}
複製代碼
直接來吧,「三步曲」 拆起來:
func f3() (r int) {
// 1.賦值
r = 1
// 2.r 做爲函數參數,不會修改要返回的那個 r 值
defer func(r int) {
r = r + 5
}(r)
// 3.直接 return
return
}
func main() {
fmt.Println(f3())
}
複製代碼
這裏注意一下,在第二步中,r 做爲函數參數使用,是值的複製,所以 defer 函數體內的 r 和 外部的 r 是兩個變量,defer 函數體內變量 r 的改變不會影響 外部變量 r ,因此函數 f3() 的返回值應該是 1。
上面的例子咱們討論的是函數顯示返回值方式(帶命名返回參數),那若是函數匿名返回值呢?咱們再看看文章開頭的例子:
func increaseA() int {
var i int
defer func() {
i++
}()
return i
}
複製代碼
咱們能夠看到 函數increaseA() 是匿名返回值,直接返回局部變量,同時 defer 函數對局部變量是閉包引用,defer 函數也會操做這個局部變量。
一樣的,咱們依然是根據 return 「三步曲」 走,對於匿名返回值,咱們能夠假設有一個變量存儲返回值,假設返回值變量爲 dashu,那麼上面代碼是否是能夠拆解爲:
dashu = i
i++
return
複製代碼
因此當程序編譯 return i 時,首先會把 變量 i 的值拷貝給 變量 dashu,儘管 defer 是閉包引用的方式,而且在 defer 函數中修改了變量 i 的值,但對返回值 dashu 不形成影響,因此最終函數 increaseA() 返回0。
最後一個細節,若是defer的函數的參數中又以某個函數的返回值爲看成參數,那麼 defer 又是怎麼表現的呢?能夠參考大叔公衆號文章:傳送門
好了,以上就是關於 defer 使用細節的所有內容。有收穫的小夥伴點個讚唄,3Q~
關注公衆號「大叔說碼」 跟大叔一塊兒打基礎,咱們下期見~