天天看點

詳解defer實作機制(附上三道面試題,我不信你們都能做對)

前言

!! 嗨,大家好,我是asong,鴿了好久,其實元旦就想寫一下這篇文章,但是因為喝酒喝斷片了,養了三天才緩過來,就推遲到這個周末了,不過多追溯了,有點丢人。今天與大家來聊一聊​

​go​

​​中的關鍵字​

​defer​

​​,目前很多程式設計語言中都有​

​defer​

​​關鍵字,而​

​go​

​​語言的​

​defer​

​​用于資源的釋放,會在函數傳回之前進行調用,它會經常被用于關閉檔案描述符、關閉資料庫連接配接以及解鎖資源。下面我們就深入​

​Go​

​​語言源碼介紹​

​defer​

​關鍵字的實作原理。文末尾給你們留了三道題,檢測一下學習成果吧~

基本使用

我們首先來看一看​

​defer​

​關鍵字是怎麼使用的,一個經典的場景就是我們在使用事務時,發生錯誤需要復原,這時我們就可以用使用defer來保證程式退出時保證事務復原,示例代碼如下:

// 代碼摘自之前寫的 Leaf-segment資料庫擷取ID方案:https://github.com/asong2020/go-algorithm/blob/master/leaf/dao/leaf_dao.go
func (l *LeafDao) NextSegment(ctx context.Context, bizTag string) (*model.Leaf, error) {
 // 開啟事務
 tx, err := l.sql.Begin()
 defer func() {
  if err != nil {
   l.rollback(tx)
  }
 }()
 if err = l.checkError(err); err != nil {
  return nil, err
 }
 err = l.db.UpdateMaxID(ctx, bizTag, tx)
 if err = l.checkError(err); err != nil {
  return nil, err
 }
 leaf, err := l.db.Get(ctx, bizTag, tx)
 if err = l.checkError(err); err != nil {
  return nil, err
 }
 // 送出事務
 err = tx.Commit()
 if err = l.checkError(err); err != nil {
  return nil, err
 }
 return leaf, nil
}      

上面隻是一個簡單的應用,​

​defer​

​還有一些特性,如果你不知道,使用起來可能會踩到一些坑,尤其是跟帶命名的傳回參數一起使用時。下面我們我先來帶大家踩踩坑。

​defer​

​的注意事項和細節

​defer​

​調用順序

我們先來看一道題,你能說他的答案嗎?

func main() {
    fmt.Println("reciprocal")

    for i := 0; i < 10; i++ {
        defer fmt.Println(i)
    }
}      

答案:

reciprocal
9
8
7
6
5
4
3
2
1
0      

看到答案,你是不是産生了疑問?這就對了,我最開始學​

​golang​

​​時也有這個疑問,這個跟棧一樣,即"先進後出"特性,越後面的defer表達式越先被調用。是以這裡大家關閉依賴資源時一定要注意​

​defer​

​調用順序。

​defer​

​拷貝

我們先來看這樣一段代碼,你能說出​

​defer​

​​中​

​num1​

​​和​

​num2​

​的值是多少嗎?

func main() {
 fmt.Println(Sum(1, 2))
}

func Sum(num1, num2 int) int {
 defer fmt.Println("num1:", num1)
 defer fmt.Println("num2:", num2)
 num1++
 num2++
 return num1 + num2
}      

聰明的你一定會說:"這也太簡單了,答案就是num1等于2,num2等于3"。很遺憾的告訴你,錯了,正确的答案是​

​num1​

​​為​

​1​

​​,​

​num2​

​​為2,這兩個變量并不受​

​num1++、num2++​

​​的影響,因為​

​defer​

​将語句放入到棧中時,也會将相關的值拷貝同時入棧。

​defer​

​​與​

​return​

​的傳回時機

這裡我先說結論,總結一下就是,函數的整個傳回過程應該是:

  1. ​return​

    ​ 對傳回變量指派,如果是匿名傳回值就先聲明再指派;
  2. 執行​

    ​defer​

    ​ 函數;
  3. ​return​

    ​ 攜帶傳回值傳回。

下面我們來看兩道題,你知道他們的傳回值是多少嗎?

  • 匿名傳回值函數
// 匿名函數
func Anonymous() int {
 var i int
 defer func() {
  i++
  fmt.Println("defer2 value is ", i)
 }()

 defer func() {
  i++
  fmt.Println("defer1 in value is ", i)
 }()

 return i
}      
  • 命名傳回值的函數
func HasName() (j int) {
 defer func() {
  j++
  fmt.Println("defer2 in value", j)
 }()

 defer func() {
  j++
  fmt.Println("defer1 in value", j)
 }()

 return j
}      

先來公布一下答案吧:

1. Anonymous()的傳回值為0
2. HasName()的傳回值為2      

從這我們可以看出命名傳回值的函數的傳回值被 ​

​defer​

​​ 修改了。這裡想必大家跟我一樣,都很疑惑,帶着疑惑我查閱了一下​

​go​

​​官方文檔,文檔指出,​

​defer​

​的執行順序有以下三個規則:

  1. A deferred function’s arguments are evaluated when the defer statement is evaluated.
  2. Deferred function calls are executed in Last In First Out order after the surrounding function returns.
  3. Deferred functions may read and assign to the returning function’s named return values.

規則3就可以印證為什麼命名傳回值的函數的傳回值被更改了,其實在函數最終傳回前,​

​defer​

​​ 函數就已經執行了,在命名傳回值的函數 中,由于傳回值已經被提前聲明,是以 ​

​defer​

​​ 函數能夠在 ​

​return​

​​ 語句對傳回值指派之後,繼續對傳回值進行操作,操作的是同一個變量,而匿名傳回值函數中return先傳回,已經進行了一次值拷貝r=i,​

​defer​

​​函數中再次對變量​

​i​

​的操作并不會影響傳回值。

這裡可能有些小夥伴還不是很懂,我在講一下​

​return​

​傳回步驟,相信你們會豁然開朗。

  • 函數在傳回時,首先函數傳回時會自動建立一個傳回變量假設為ret(如果是命名傳回值的函數則不會建立),函數傳回時要将變量​

    ​i​

    ​指派給ret,即有ret = i。
  • 然後檢查函數中是否有defer存在,若有則執行defer中部分。
  • 最後傳回ret

現在你們應該知道上面是什麼原因了吧~。

解密​

​defer​

​源碼

寫在開頭:go版本1.15.3

我們先來寫一段代碼,檢視一下彙編代碼:

func main() {
 defer func() {
  fmt.Println("asong 真帥")
 }()
}      

執行如下指令:​

​go tool compile -N -l -S main.go​

​,截取部分彙編指令如下:

詳解defer實作機制(附上三道面試題,我不信你們都能做對)

我們可以看出來,從執行流程來看首先會調用​

​deferproc​

​​來建立​

​defer​

​​,然後在函數傳回時插入了指令​

​CALL runtime.deferreturn​

​​。知道了​

​defer​

​​在流程中是通過這兩個方法是調用的,接下來我們來看一看​

​defer​

​的結構:

// go/src/runtime/runtime2.go
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
}      

這裡簡單介紹一下​

​runtime._defer​

​結構體中的幾個字段:

  • ​siz​

    ​代表的是參數和結果的記憶體大小
  • ​sp​

    ​​和​

    ​pc​

    ​分别代表棧指針和調用方的程式計數器
  • ​fn​

    ​​代表的是​

    ​defer​

    ​關鍵字中傳入的函數
  • ​_panic​

    ​是觸發延遲調用的結構體,可能為空
  • ​openDefer​

    ​​表示的是目前​

    ​defer​

    ​是否已經開放編碼優化(1.14版本新增)
  • ​link​

    ​​所有​

    ​runtime._defer​

    ​結構體都通過該字段串聯成連結清單

先來我們也知道了​

​defer​

​​關鍵字的資料結構了,下面我們就來重點分析一下​

​deferproc​

​​和​

​deferreturn​

​函數是如何調用。

​deferproc​

​函數

​deferproc​

​函數也不長,我先貼出來代碼;

// proc/panic.go
// Create a new deferred function fn with siz bytes of arguments.
// The compiler turns a defer statement into a call to this.
//go:nosplit
func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
 gp := getg()
 if gp.m.curg != gp {
  // go code on the system stack can't defer
  throw("defer on system stack")
 }

 // the arguments of fn are in a perilous state. The stack map
 // for deferproc does not describe them. So we can't let garbage
 // collection or stack copying trigger until we've copied them out
 // to somewhere safe. The memmove below does that.
 // Until the copy completes, we can only call nosplit routines.
 sp := getcallersp()
 argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
 callerpc := getcallerpc()

 d := newdefer(siz)
 if d._panic != nil {
  throw("deferproc: d.panic != nil after newdefer")
 }
 d.link = gp._defer
 gp._defer = d
 d.fn = fn
 d.pc = callerpc
 d.sp = sp
 switch siz {
 case 0:
  // Do nothing.
 case sys.PtrSize:
  *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
 default:
  memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
 }

 // deferproc returns 0 normally.
 // a deferred func that stops a panic
 // makes the deferproc return 1.
 // the code the compiler generates always
 // checks the return value and jumps to the
 // end of the function if deferproc returns != 0.
 return0()
 // No code can go here - the C return register has
 // been set and must not be clobbered.
}      

上面介紹了​

​rumtiem._defer​

​結構想必這裡的入參是什麼意思就不用我介紹了吧。

​deferproc​

​​的函數流程很清晰,首先他會通過​

​newdefer​

​​函數配置設定一個​

​_defer​

​​結構對象,然後把需要延遲執行的函數以及該函數需要用到的參數、調用​

​deferproc​

​​函數時的​

​rps​

​​寄存器的值以及​

​deferproc​

​​函數的傳回位址儲存在​

​_defer​

​​結構體對象中,最後通過​

​return0()​

​​設定​

​rax​

​​寄存器的值為0隐性的給調用者傳回一個0值。​

​deferproc​

​​主要是靠​

​newdefer​

​​來配置設定​

​_defer​

​​結構體對象的,下面我們一起來看看​

​newdefer​

​實作,代碼有點長:

// proc/panic.go
// Allocate a Defer, usually using per-P pool.
// Each defer must be released with freedefer.  The defer is not
// added to any defer chain yet.
//
// This must not grow the stack because there may be a frame without
// stack map information when this is called.
//
//go:nosplit
func newdefer(siz int32) *_defer {
 var d *_defer
 sc := deferclass(uintptr(siz))
 gp := getg()//擷取目前goroutine的g結構體對象
 if sc < uintptr(len(p{}.deferpool)) {
  pp := gp.m.p.ptr() //與目前工作線程綁定的p
  if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
   // Take the slow path on the system stack so
   // we don't grow newdefer's stack.
   systemstack(func() {
    lock(&sched.deferlock) 
         //把新配置設定出來的d放入目前goroutine的_defer連結清單頭
    for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
     d := sched.deferpool[sc]
     sched.deferpool[sc] = d.link
     d.link = nil
     pp.deferpool[sc] = append(pp.deferpool[sc], d)
    }
    unlock(&sched.deferlock)
   })
  }
  if n := len(pp.deferpool[sc]); n > 0 {
   d = pp.deferpool[sc][n-1]
   pp.deferpool[sc][n-1] = nil
   pp.deferpool[sc] = pp.deferpool[sc][:n-1]
  }
 }
 if d == nil {
    //如果p的緩存中沒有可用的_defer結構體對象則從堆上配置設定
         // Allocate new defer+args.
         //因為roundupsize以及mallocgc函數都不會處理擴棧,是以需要切換到系統棧執行
  // Allocate new defer+args.
  systemstack(func() {
   total := roundupsize(totaldefersize(uintptr(siz)))
   d = (*_defer)(mallocgc(total, deferType, true))
  })
  if debugCachedWork {
   // Duplicate the tail below so if there's a
   // crash in checkPut we can tell if d was just
   // allocated or came from the pool.
   d.siz = siz
       //把新配置設定出來的d放入目前goroutine的_defer連結清單頭
   d.link = gp._defer
   gp._defer = d
   return d
  }
 }
 d.siz = siz
 d.heap = true
 return d
}      

​newdefer​

​​函數首先會嘗試從目前工作線程綁定的​

​p​

​​的​

​_defer​

​​對象池和全局對象池中擷取一個滿足大小要求​

​(sizeof(_defer) + siz向上取整至16的倍數)​

​​的​

​_defer​

​​ 結構體對象,如果沒有能夠滿足要求的空閑 ​

​_defer​

​​對象則從堆上分一個,最後把配置設定到的對象鍊入目前 ​

​goroutine​

​​的​

​_defer​

​ 連結清單的表頭。

到此​

​deferproc​

​函數就分析完了,你們懂了嗎? 沒懂不要緊,我們再來總結一下這個過程:

  • 首先編譯器把​

    ​defer​

    ​語句翻譯成對應的​

    ​deferproc​

    ​函數的調用
  • 然後​

    ​deferproc​

    ​函數通過​

    ​newdefer​

    ​函數配置設定一個​

    ​_defer​

    ​結構體對象并放入目前的​

    ​goroutine​

    ​的​

    ​_defer​

    ​連結清單的表頭;
  • 在 _defer 結構體對象中儲存被延遲執行的函數 fn 的位址以及 fn 所需的參數
  • 傳回到調用 deferproc 的函數繼續執行後面的代碼。

​deferreturn​

​函數

// Run a deferred function if there is one.
// The compiler inserts a call to this at the end of any
// function which calls defer.
// If there is a deferred function, this will call runtime·jmpdefer,
// which will jump to the deferred function such that it appears
// to have been called by the caller of deferreturn at the point
// just before deferreturn was called. The effect is that deferreturn
// is called again and again until there are no more deferred functions.
//
// Declared as nosplit, because the function should not be preempted once we start
// modifying the caller's frame in order to reuse the frame to call the deferred
// function.
//
// The single argument isn't actually used - it just has its address
// taken so it can be matched against pending defers.
//go:nosplit
func deferreturn(arg0 uintptr) {
 gp := getg() //擷取目前goroutine對應的g結構體對象
 d := gp._defer //擷取目前goroutine對應的g結構體對象
 if d == nil {
    //沒有需要執行的函數直接傳回,deferreturn和deferproc是配對使用的
         //為什麼這裡d可能為nil?因為deferreturn其實是一個遞歸調用,這個是遞歸結束條件之一
  return
 }
 sp := getcallersp() //擷取調用deferreturn時的棧頂位置
 if d.sp != sp { // 遞歸結束條件
    //如果儲存在_defer對象中的sp值與調用deferretuen時的棧頂位置不一樣,直接傳回
        //因為sp不一樣表示d代表的是在其他函數中通過defer注冊的延遲調用函數,比如:
        //a()->b()->c()它們都通過defer注冊了延遲函數,那麼當c()執行完時隻能執行在c中注冊的函數
  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.
      //把儲存在_defer對象中的fn函數需要用到的參數拷貝到棧上,準備調用fn
    //注意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 // 使gp._defer指向下一個_defer結構體對象
    //因為需要調用的函數d.fn已經儲存在了fn變量中,它的參數也已經拷貝到了棧上,是以釋放_defer結構體對象
 freedefer(d)
 // 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)))
}      

​deferreturn​

​函數主要流程還是簡單一些的,我們來分析一下:

  • 首先我們通過目前​

    ​goroutine​

    ​對應的​

    ​g​

    ​結構體對象的​

    ​_defer​

    ​連結清單判斷是否有需要執行的​

    ​defered​

    ​函數,如果沒有則傳回;這裡的沒有是指g._defer== nil 或者​

    ​defered​

    ​函數不是在​

    ​deferteturn​

    ​的​

    ​caller​

    ​函數中注冊的函數。
  • 然後我們在從​

    ​_defer​

    ​對象中把​

    ​defered​

    ​函數需要的參數拷貝到棧上,并釋放​

    ​_defer​

    ​的結構體對象。
  • 最紅調用​

    ​jmpderfer​

    ​函數調用​

    ​defered​

    ​函數,也就是​

    ​defer​

    ​關鍵字中傳入的函數.

​jmpdefer​

​函數實作挺優雅的,我們一起來看看他是如何實作的:

// runtime/asm_amd64.s : 581
// func jmpdefer(fv *funcval, argp uintptr)
// argp is a caller SP.
// called from deferreturn.
// 1. pop the caller
// 2. sub 5 bytes from the callers return
// 3. jmp to the argument
TEXT runtime·jmpdefer(SB), NOSPLIT, $0-16
 MOVQ fv+0(FP), DX // fn
 MOVQ argp+8(FP), BX // caller sp
 LEAQ -8(BX), SP // caller sp after CALL
 MOVQ -8(SP), BP // restore BP as if deferreturn returned (harmless if framepointers not in use)
 SUBQ $5, (SP) // return to CALL again
 MOVQ 0(DX), BX
 JMP BX // but first run the deferred function      

這裡都是彙編,大家可能看不懂,沒關系,我隻是簡單介紹一下這裡,有興趣的同學可以去查閱一下相關知識再來深入了解。

  • ​MOVQ fv+0(FP), DX​

    ​​這條指令會把​

    ​jmpdefer​

    ​的第一個參數也就是結構體對象​

    ​fn​

    ​的位址存放入​

    ​DX​

    ​寄存器,之後的代碼就可以通過寄存器通路到​

    ​fn​

    ​,​

    ​fn​

    ​就可以拿到​

    ​defer​

    ​關鍵字中傳入的函數,對應上面的例子就是匿名函數​

    ​func(){}()​

    ​.
  • ​MOVQ argp+8(FP), BX​

    ​​這條指令就是把​

    ​jmpdefer​

    ​的第二個參數放入​

    ​BX​

    ​寄存器,該參數是一個指針,他指向​

    ​defer​

    ​關鍵字中傳入的函數的第一個參數.
  • ​LEAQ -8(BX), SP​

    ​​這條指令的作用是讓 ​

    ​SP​

    ​ 寄存器指向 ​

    ​deferreturn​

    ​ 函數的傳回位址所在的棧記憶體單元.
  • ​MOVQ -8(SP), BP​

    ​​這條指令的作用是調整 BP 寄存器的值,此時​

    ​SP_8​

    ​的位置存放的是​

    ​defer​

    ​關鍵字目前所在的函數的​

    ​rbp​

    ​寄存器的值,是以這條指令在調整​

    ​rbp​

    ​寄存器的值使其指向目前所在函數的棧幀的适當位置.
  • ​SUBQ $5, (SP)​

    ​​這裡的作用是完成​

    ​defer​

    ​函數的參數以及執行完函數後傳回位址在棧上的構造.因為在執行這條指令時,rsp寄存器指向的是​

    ​deferreturn​

    ​函數的傳回位址.
  • ​MOVQ 0(DX), BX​

    ​​和​

    ​JMP BX​

    ​放到一起說吧,目的是跳轉到對應​

    ​defer​

    ​函數去執行,完成​

    ​defer​

    ​函數的調用.

總結

大概分析了一下​

​defer​

​的實作機制,但還是有點蒙圈,最後在總結一下這裡:

  • 首先編譯器會把​

    ​defer​

    ​語句翻譯成對​

    ​deferproc​

    ​函數的調用。
  • 然後​

    ​deferproc​

    ​函數會負責調用​

    ​newdefer​

    ​函數配置設定一個​

    ​_defer​

    ​結構體對象并放入目前的​

    ​goroutine​

    ​的​

    ​_defer​

    ​連結清單的表頭;
  • 然後編譯起會在​

    ​defer​

    ​所在函數的結尾處插入對​

    ​deferreturn​

    ​的調用,​

    ​deferreturn​

    ​負責遞歸的調用某函數(defer語句所在函數)通過​

    ​defer​

    ​語句注冊的函數。

總體來說就是這三個步驟,go語言對​

​defer​

​的實作機制就是這樣啦,你明白了嗎?

小試牛刀

上面我們也細緻學習了一下​

​defer​

​,下面出幾道題吧,看看你們真的學會了嗎?

問題1

// 測試1
func Test1() (r int) {
 i := 1
 defer func() {
  i = i + 1
 }()
 return i
}      

傳回結果是什麼?

問題2

func Test2() (r int) {
 defer func(r int) {
  r = r + 2
 }(r)
 return 2
}      

傳回結果是什麼?

如果改成這樣呢?

func Test3() (r int) {
 defer func(r *int) {
  *r = *r + 2
 }(&r)
 return 2
}      

問題3

func main(){
  e1()
  e2()
  e3()
}
func e1() {
 var err error
 defer fmt.Println(err)
 err = errors.New("e1 defer err")
}

func e2() {
 var err error
 defer func() {
  fmt.Println(err)
 }()
 err = errors.New("e2 defer err")
}

func e3() {
 var err error
 defer func(err error) {
  fmt.Println(err)
 }(err)
 err = errors.New("e3 defer err")
}      

這個傳回結果又是什麼呢?

總結

最後這三道題這裡就當作思考題吧,自己運作一下,看看你們想的和運作結果是否一緻呢?