關於defer坑坑窪窪的使用細節你mark了嗎

「第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是什麼

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
}
複製代碼

整個執行流程能夠理解爲:

  • 判斷當前goroutine上是否還有綁定的defer,若沒有,直接return;
  • 獲取 goroutine 綁定的 defer 鏈表頭部的defer;
  • 判斷當前 defer 中存儲的sp是否和調用者的sp一致,若不一致,也直接return,證實當前defer不是在此調用函數中聲明的;
  • 進行參數的拷貝;
  • 釋放當前要執行的fn關聯的defer;
  • 執行 jmpdefer 函數,這裏執行完fn的邏輯後會遞歸調用 deferreturn 函數。

坑坑窪窪的使用細節

上面總體介紹了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 又是怎麼表現的呢?能夠參考大叔公衆號文章:傳送門

好了,以上就是關於 defer 使用細節的所有內容。有收穫的小夥伴點個讚唄,3Q~

關注公衆號「大叔說碼」 跟大叔一塊兒打基礎,咱們下期見~

相關文章
相關標籤/搜索