https://mp.weixin.qq.com/s/FUmoBB8OHNSfy7STR0GsWw

基本使用

我们首先来看一看defer关键字是怎么使用的,一个经典的场景就是我们在使用事务时,发生错误需要回滚,这时我们就可以用使用defer来保证程序退出时保证事务回滚,示例代码如下:

  1. // 代码摘自之前写的 Leaf-segment数据库获取ID方案:https://github.com/asong2020/go-algorithm/blob/master/leaf/dao/leaf_dao.go
  2. func (l *LeafDao) NextSegment(ctx context.Context, bizTag string) (*model.Leaf, error) {
  3. // 开启事务
  4. tx, err := l.sql.Begin()
  5. defer func() {
  6. if err != nil {
  7. l.rollback(tx)
  8. }
  9. }()
  10. if err = l.checkError(err); err != nil {
  11. return nil, err
  12. }
  13. err = l.db.UpdateMaxID(ctx, bizTag, tx)
  14. if err = l.checkError(err); err != nil {
  15. return nil, err
  16. }
  17. leaf, err := l.db.Get(ctx, bizTag, tx)
  18. if err = l.checkError(err); err != nil {
  19. return nil, err
  20. }
  21. // 提交事务
  22. err = tx.Commit()
  23. if err = l.checkError(err); err != nil {
  24. return nil, err
  25. }
  26. return leaf, nil
  27. }

上面只是一个简单的应用,defer还有一些特性,如果你不知道,使用起来可能会踩到一些坑,尤其是跟带命名的返回参数一起使用时。下面我们我先来带大家踩踩坑。

defer的注意事项和细节

defer调用顺序

我们先来看一道题,你能说他的答案吗?

  1. func main() {
  2. fmt.Println("reciprocal")
  3. for i := 0; i < 10; i++ {
  4. defer fmt.Println(i)
  5. }
  6. }

答案:

  1. reciprocal
  2. 9
  3. 8
  4. 7
  5. 6
  6. 5
  7. 4
  8. 3
  9. 2
  10. 1
  11. 0

看到答案,你是不是产生了疑问?这就对了,我最开始学golang时也有这个疑问,这个跟栈一样,即”先进后出”特性,越后面的defer表达式越先被调用。所以这里大家关闭依赖资源时一定要注意defer调用顺序。

defer拷贝

我们先来看这样一段代码,你能说出defernum1num2的值是多少吗?

  1. func main() {
  2. fmt.Println(Sum(1, 2))
  3. }
  4. func Sum(num1, num2 int) int {
  5. defer fmt.Println("num1:", num1)
  6. defer fmt.Println("num2:", num2)
  7. num1++
  8. num2++
  9. return num1 + num2
  10. }

聪明的你一定会说:”这也太简单了,答案就是num1等于2,num2等于3”。很遗憾的告诉你,错了,正确的答案是num11,num2为2,这两个变量并不受num1++、num2++的影响,因为defer将语句放入到栈中时,也会将相关的值拷贝同时入栈。

deferreturn的返回时机

这里我先说结论,总结一下就是,函数的整个返回过程应该是:

  1. return 对返回变量赋值,如果是匿名返回值就先声明再赋值;
  2. 执行 defer 函数;
  3. return 携带返回值返回。

下面我们来看两道题,你知道他们的返回值是多少吗?

  • 匿名返回值函数
  1. // 匿名函数
  2. func Anonymous() int {
  3. var i int
  4. defer func() {
  5. i++
  6. fmt.Println("defer2 value is ", i)
  7. }()
  8. defer func() {
  9. i++
  10. fmt.Println("defer1 in value is ", i)
  11. }()
  12. return i
  13. }
  • 命名返回值的函数
  1. func HasName() (j int) {
  2. defer func() {
  3. j++
  4. fmt.Println("defer2 in value", j)
  5. }()
  6. defer func() {
  7. j++
  8. fmt.Println("defer1 in value", j)
  9. }()
  10. return j
  11. }

先来公布一下答案吧:

  1. 1. Anonymous()的返回值为0
  2. 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

我们先来写一段代码,查看一下汇编代码:

  1. func main() {
  2. defer func() {
  3. fmt.Println("asong 真帅")
  4. }()
  5. }

执行如下指令:go tool compile -N -l -S main.go,截取部分汇编指令如下:

我们可以看出来,从执行流程来看首先会调用deferproc来创建defer,然后在函数返回时插入了指令CALL runtime.deferreturn。知道了defer在流程中是通过这两个方法是调用的,接下来我们来看一看defer的结构:

  1. // go/src/runtime/runtime2.go
  2. type _defer struct {
  3. siz int32 // includes both arguments and results
  4. started bool
  5. heap bool
  6. // openDefer indicates that this _defer is for a frame with open-coded
  7. // defers. We have only one defer record for the entire frame (which may
  8. // currently have 0, 1, or more defers active).
  9. openDefer bool
  10. sp uintptr // sp at time of defer
  11. pc uintptr // pc at time of defer
  12. fn *funcval // can be nil for open-coded defers
  13. _panic *_panic // panic that is running defer
  14. link *_defer
  15. // If openDefer is true, the fields below record values about the stack
  16. // frame and associated function that has the open-coded defer(s). sp
  17. // above will be the sp for the frame, and pc will be address of the
  18. // deferreturn call in the function.
  19. fd unsafe.Pointer // funcdata for the function associated with the frame
  20. varp uintptr // value of varp for the stack frame
  21. // framepc is the current pc associated with the stack frame. Together,
  22. // with sp above (which is the sp associated with the stack frame),
  23. // framepc/sp can be used as pc/sp pair to continue a stack trace via
  24. // gentraceback().
  25. framepc uintptr
  26. }

这里简单介绍一下runtime._defer结构体中的几个字段:

  • siz代表的是参数和结果的内存大小
  • sppc分别代表栈指针和调用方的程序计数器
  • fn代表的是defer关键字中传入的函数
  • _panic是触发延迟调用的结构体,可能为空
  • openDefer表示的是当前defer是否已经开放编码优化(1.14版本新增)
  • link所有runtime._defer结构体都通过该字段串联成链表

先来我们也知道了defer关键字的数据结构了,下面我们就来重点分析一下deferprocdeferreturn函数是如何调用。

deferproc函数

deferproc函数也不长,我先贴出来代码;

  1. // proc/panic.go
  2. // Create a new deferred function fn with siz bytes of arguments.
  3. // The compiler turns a defer statement into a call to this.
  4. //go:nosplit
  5. func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
  6. gp := getg()
  7. if gp.m.curg != gp {
  8. // go code on the system stack can't defer
  9. throw("defer on system stack")
  10. }
  11. // the arguments of fn are in a perilous state. The stack map
  12. // for deferproc does not describe them. So we can't let garbage
  13. // collection or stack copying trigger until we've copied them out
  14. // to somewhere safe. The memmove below does that.
  15. // Until the copy completes, we can only call nosplit routines.
  16. sp := getcallersp()
  17. argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
  18. callerpc := getcallerpc()
  19. d := newdefer(siz)
  20. if d._panic != nil {
  21. throw("deferproc: d.panic != nil after newdefer")
  22. }
  23. d.link = gp._defer
  24. gp._defer = d
  25. d.fn = fn
  26. d.pc = callerpc
  27. d.sp = sp
  28. switch siz {
  29. case 0:
  30. // Do nothing.
  31. case sys.PtrSize:
  32. *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
  33. default:
  34. memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
  35. }
  36. // deferproc returns 0 normally.
  37. // a deferred func that stops a panic
  38. // makes the deferproc return 1.
  39. // the code the compiler generates always
  40. // checks the return value and jumps to the
  41. // end of the function if deferproc returns != 0.
  42. return0()
  43. // No code can go here - the C return register has
  44. // been set and must not be clobbered.
  45. }

上面介绍了rumtiem._defer结构想必这里的入参是什么意思就不用我介绍了吧。

deferproc的函数流程很清晰,首先他会通过newdefer函数分配一个_defer结构对象,然后把需要延迟执行的函数以及该函数需要用到的参数、调用deferproc函数时的rps寄存器的值以及deferproc函数的返回地址保存在_defer结构体对象中,最后通过return0()设置rax寄存器的值为0隐性的给调用者返回一个0值。deferproc主要是靠newdefer来分配_defer结构体对象的,下面我们一起来看看newdefer实现,代码有点长:

  1. // proc/panic.go
  2. // Allocate a Defer, usually using per-P pool.
  3. // Each defer must be released with freedefer. The defer is not
  4. // added to any defer chain yet.
  5. //
  6. // This must not grow the stack because there may be a frame without
  7. // stack map information when this is called.
  8. //
  9. //go:nosplit
  10. func newdefer(siz int32) *_defer {
  11. var d *_defer
  12. sc := deferclass(uintptr(siz))
  13. gp := getg()//获取当前goroutine的g结构体对象
  14. if sc < uintptr(len(p{}.deferpool)) {
  15. pp := gp.m.p.ptr() //与当前工作线程绑定的p
  16. if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil {
  17. // Take the slow path on the system stack so
  18. // we don't grow newdefer's stack.
  19. systemstack(func() {
  20. lock(&sched.deferlock)
  21. //把新分配出来的d放入当前goroutine的_defer链表头
  22. for len(pp.deferpool[sc]) < cap(pp.deferpool[sc])/2 && sched.deferpool[sc] != nil {
  23. d := sched.deferpool[sc]
  24. sched.deferpool[sc] = d.link
  25. d.link = nil
  26. pp.deferpool[sc] = append(pp.deferpool[sc], d)
  27. }
  28. unlock(&sched.deferlock)
  29. })
  30. }
  31. if n := len(pp.deferpool[sc]); n > 0 {
  32. d = pp.deferpool[sc][n-1]
  33. pp.deferpool[sc][n-1] = nil
  34. pp.deferpool[sc] = pp.deferpool[sc][:n-1]
  35. }
  36. }
  37. if d == nil {
  38. //如果p的缓存中没有可用的_defer结构体对象则从堆上分配
  39. // Allocate new defer+args.
  40. //因为roundupsize以及mallocgc函数都不会处理扩栈,所以需要切换到系统栈执行
  41. // Allocate new defer+args.
  42. systemstack(func() {
  43. total := roundupsize(totaldefersize(uintptr(siz)))
  44. d = (*_defer)(mallocgc(total, deferType, true))
  45. })
  46. if debugCachedWork {
  47. // Duplicate the tail below so if there's a
  48. // crash in checkPut we can tell if d was just
  49. // allocated or came from the pool.
  50. d.siz = siz
  51. //把新分配出来的d放入当前goroutine的_defer链表头
  52. d.link = gp._defer
  53. gp._defer = d
  54. return d
  55. }
  56. }
  57. d.siz = siz
  58. d.heap = true
  59. return d
  60. }

newdefer函数首先会尝试从当前工作线程绑定的p_defer对象池和全局对象池中获取一个满足大小要求(sizeof(_defer) + siz向上取整至16的倍数)_defer 结构体对象,如果没有能够满足要求的空闲 _defer对象则从堆上分一个,最后把分配到的对象链入当前 goroutine_defer 链表的表头。

到此deferproc函数就分析完了,你们懂了吗? 没懂不要紧,我们再来总结一下这个过程:

  • 首先编译器把defer语句翻译成对应的deferproc函数的调用
  • 然后deferproc函数通过newdefer函数分配一个_defer结构体对象并放入当前的goroutine_defer链表的表头;
  • 在 _defer 结构体对象中保存被延迟执行的函数 fn 的地址以及 fn 所需的参数
  • 返回到调用 deferproc 的函数继续执行后面的代码。

deferreturn函数

  1. // Run a deferred function if there is one.
  2. // The compiler inserts a call to this at the end of any
  3. // function which calls defer.
  4. // If there is a deferred function, this will call runtime·jmpdefer,
  5. // which will jump to the deferred function such that it appears
  6. // to have been called by the caller of deferreturn at the point
  7. // just before deferreturn was called. The effect is that deferreturn
  8. // is called again and again until there are no more deferred functions.
  9. //
  10. // Declared as nosplit, because the function should not be preempted once we start
  11. // modifying the caller's frame in order to reuse the frame to call the deferred
  12. // function.
  13. //
  14. // The single argument isn't actually used - it just has its address
  15. // taken so it can be matched against pending defers.
  16. //go:nosplit
  17. func deferreturn(arg0 uintptr) {
  18. gp := getg() //获取当前goroutine对应的g结构体对象
  19. d := gp._defer //获取当前goroutine对应的g结构体对象
  20. if d == nil {
  21. //没有需要执行的函数直接返回,deferreturn和deferproc是配对使用的
  22. //为什么这里d可能为nil?因为deferreturn其实是一个递归调用,这个是递归结束条件之一
  23. return
  24. }
  25. sp := getcallersp() //获取调用deferreturn时的栈顶位置
  26. if d.sp != sp { // 递归结束条件
  27. //如果保存在_defer对象中的sp值与调用deferretuen时的栈顶位置不一样,直接返回
  28. //因为sp不一样表示d代表的是在其他函数中通过defer注册的延迟调用函数,比如:
  29. //a()->b()->c()它们都通过defer注册了延迟函数,那么当c()执行完时只能执行在c中注册的函数
  30. return
  31. }
  32. if d.openDefer {
  33. done := runOpenDeferFrame(gp, d)
  34. if !done {
  35. throw("unfinished open-coded defers in deferreturn")
  36. }
  37. gp._defer = d.link
  38. freedefer(d)
  39. return
  40. }
  41. // Moving arguments around.
  42. //
  43. // Everything called after this point must be recursively
  44. // nosplit because the garbage collector won't know the form
  45. // of the arguments until the jmpdefer can flip the PC over to
  46. // fn.
  47. //把保存在_defer对象中的fn函数需要用到的参数拷贝到栈上,准备调用fn
  48. //注意fn的参数放在了调用调用者的栈帧中,而不是此函数的栈帧中
  49. switch d.siz {
  50. case 0:
  51. // Do nothing.
  52. case sys.PtrSize:
  53. *(*uintptr)(unsafe.Pointer(&arg0)) = *(*uintptr)(deferArgs(d))
  54. default:
  55. memmove(unsafe.Pointer(&arg0), deferArgs(d), uintptr(d.siz))
  56. }
  57. fn := d.fn
  58. d.fn = nil
  59. gp._defer = d.link // 使gp._defer指向下一个_defer结构体对象
  60. //因为需要调用的函数d.fn已经保存在了fn变量中,它的参数也已经拷贝到了栈上,所以释放_defer结构体对象
  61. freedefer(d)
  62. // If the defer function pointer is nil, force the seg fault to happen
  63. // here rather than in jmpdefer. gentraceback() throws an error if it is
  64. // called with a callback on an LR architecture and jmpdefer is on the
  65. // stack, because the stack trace can be incorrect in that case - see
  66. // issue #8153).
  67. _ = fn.fn
  68. jmpdefer(fn, uintptr(unsafe.Pointer(&arg0)))
  69. }

deferreturn函数主要流程还是简单一些的,我们来分析一下:

  • 首先我们通过当前goroutine对应的g结构体对象的_defer链表判断是否有需要执行的defered函数,如果没有则返回;这里的没有是指g._defer== nil 或者defered函数不是在deferteturncaller函数中注册的函数。
  • 然后我们在从_defer对象中把defered函数需要的参数拷贝到栈上,并释放_defer的结构体对象。
  • 最红调用jmpderfer函数调用defered函数,也就是defer关键字中传入的函数.

jmpdefer函数实现挺优雅的,我们一起来看看他是如何实现的:

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

这里都是汇编,大家可能看不懂,没关系,我只是简单介绍一下这里,有兴趣的同学可以去查阅一下相关知识再来深入了解。

  • MOVQ fv+0(FP), DX这条指令会把jmpdefer的第一个参数也就是结构体对象fn的地址存放入DX寄存器,之后的代码就可以通过寄存器访问到fnfn就可以拿到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), BXJMP BX放到一起说吧,目的是跳转到对应defer函数去执行,完成defer函数的调用.

defer执行效率问题

defer 关键字其实涉及了一系列的连锁调用,内部 runtime 函数的调用就至少多了三步,分别是 runtime.deferproc 一次和 runtime.deferreturn 两次。
而这还只是在运行时的显式动作,另外编译器做的事也不少,例如:

  • 在 deferprocStack或者deferproc 阶段(注册延迟调用),还得获取/传入目标函数地址、函数参数等等。
  • 在 deferreturn 阶段,需要在函数调用结尾处插入该方法的调用,同时若有被 defer 的函数,还需要使用 runtime·jmpdefer 进行跳转以便于后续调用。

这一些动作途中还要涉及最小单元 _defer 的获取/创建(在堆或栈), defer 和 recover 链表的逻辑处理和消耗等动作。

https://segmentfault.com/a/1190000019490834
https://segmentfault.com/a/1190000019303572

总结

大概分析了一下defer的实现机制,但还是有点蒙圈,最后在总结一下这里:

  • 首先编译器会把defer语句翻译成对deferproc函数的调用。
  • 然后deferproc函数会负责调用newdefer函数分配一个_defer结构体对象并放入当前的goroutine_defer链表的表头;
  • 然后编译起会在defer所在函数的结尾处插入对deferreturn的调用,deferreturn负责递归的调用某函数(defer语句所在函数)通过defer语句注册的函数。

总体来说就是这三个步骤,go语言对defer的实现机制就是这样了?