一篇短文介绍 Defer 是如何工作的

Go语言精选

共 503字,需浏览 2分钟

 ·

2020-09-19 08:20

点击上方蓝色“Go语言中文网”关注我们,领全套Go资料,每天学习 Go 语言

Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ 这篇文章基于 Go 1.12。

`defer` 语句[1]是在函数返回前执行一段代码的便捷方法,如 Golang 规范[2]所描述:

延迟函数( deferred functions )在所在函数返回前,以与声明相反的顺序立即被调用

以下是 LIFO (后进先出)实现的例子:

func main() {
   defer func() {
      println(`defer 1`)
   }()
   defer func() {
      println(`defer 2`)
   }()
}
defer 2 <- 后进先出
defer 1

来看一下内部的实现,然后再看一个更复杂的案例。

内部实现

Go 运行时(runtime)使用一个链表来实现 LIFO。实际上,一个 defer 结构体持有一个指向下一个要被执行的 defer 结构体的指针:

type _defer struct {
   siz     int32
   started bool
   sp      uintptr
   pc      uintptr
   fn      *funcval
   _panic  *_panic
   link    *_defer // 下一个要被执行的延迟函数

当一个新的 defer 方法被创建的时候,它被附加到当前的 Goroutine 上,然后之前的 defer 方法作为下一个要执行的函数被链接到新创建的方法上:

func newdefer(siz int32) *_defer {
   var d *_defer
   gp := getg() // 获取当前 goroutine
   [...]
   // 延迟列表现在被附加到新的 _defer 结构体
   d.link = gp._defer
   gp._defer = d // 新的结构现在是第一个被调用的
   return d
}

现在,后续调用会从栈的顶部依次出栈延迟函数:

func deferreturn(arg0 uintptr) {
   gp := getg() // 获取当前 goroutine
   d:= gp._defer // 拷贝延迟函数到一个变量上
   if d == nil { // 如果不存在延迟函数就直接返回
      return
   }
   [...]
   fn := d.fn // 获取要调用的函数
   d.fn = nil // 重置函数
   gp._defer = d.link // 把下一个 _defer 结构体依附到 Goroutine 上
   freedefer(d) // 释放 _defer 结构体
   jmpdefer(fn, uintptr(unsafe.Pointer(&arg0))) // 调用该函数
}

如我们所见,并没有循环地去调用延迟函数,而是一个接一个地出栈。这一行为可以通过生成汇编[3]代码得到验证:

// 第一个延迟函数
0x001d 00029 (main.go:6) MOVL $0, (SP)
0x0024 00036 (main.go:6) PCDATA $2, $1
0x0024 00036 (main.go:6) LEAQ "".main.func1·f(SB), AX
0x002b 00043 (main.go:6) PCDATA $2, $0
0x002b 00043 (main.go:6) MOVQ AX, 8(SP)
0x0030 00048 (main.go:6) CALL runtime.deferproc(SB)
0x0035 00053 (main.go:6) TESTL AX, AX
0x0037 00055 (main.go:6) JNE 117
// 第二个延迟函数
0x0039 00057 (main.go:10) MOVL $0, (SP)
0x0040 00064 (main.go:10) PCDATA $2, $1
0x0040 00064 (main.go:10) LEAQ "".main.func2·f(SB), AX
0x0047 00071 (main.go:10) PCDATA $2, $0
0x0047 00071 (main.go:10) MOVQ AX, 8(SP)
0x004c 00076 (main.go:10) CALL runtime.deferproc(SB)
0x0051 00081 (main.go:10) TESTL AX, AX
0x0053 00083 (main.go:10) JNE 101
// main 函数结束
0x0055 00085 (main.go:18) XCHGL AX, AX
0x0056 00086 (main.go:18) CALL runtime.deferreturn(SB)
0x005b 00091 (main.go:18) MOVQ 16(SP), BP
0x0060 00096 (main.go:18) ADDQ $24, SP
0x0064 00100 (main.go:18) RET
0x0065 00101 (main.go:10) XCHGL AX, AX
0x0066 00102 (main.go:10) CALL runtime.deferreturn(SB)
0x006b 00107 (main.go:10) MOVQ 16(SP), BP
0x0070 00112 (main.go:10) ADDQ $24, SP
0x0074 00116 (main.go:10) RET

deferproc 方法被调用了两次,并且内部调用了 newdefer 方法,我们之前已经看到该方法将我们的函数注册为延迟函数。之后,在函数的最后,在 deferreturn 函数的帮助下,延迟方法会被一个接一个地调用。

Go 标准库向我们展示了结构体 _defer 同样链接了一个 _panic *_panic 属性。来通过另一个例子看下它在哪里会起作用。

延迟和返回值

如规范所描述,延迟函数访问返回的结果的唯一方法是使用命名返回参数[4]

如果延迟函数是一个匿名函数( function literal )[5],并且所在函数存在命名返回参数[6],同时该命名返回参数在匿名函数的作用域中,匿名函数可能会在返回参数返回前访问并修改它们。

这里有个例子:

func main() {
   fmt.Printf("with named param, x: %d\n", namedParam())
   fmt.Printf("without named param, x: %d\n", notNamedParam())
}
func namedParam() (x int) {
   x = 1
   defer func() { x = 2 }()
   return x
}

func notNamedParam() (int) {
   x := 1
   defer func() { x = 2 }()
   return x
}
with named param, x: 2
without named param, x: 1

确实就像这篇“defer, panic 和 recover[7]”博客所描述的一样,一旦确定这一行为,我们可以将其与 recover 函数混合使用:

recover 函数 是一个用于重新获取对恐慌(panicking)goroutine 控制的内置函数。recover 函数仅在延迟函数内部时才有效。

如我们所见,_defer 结构体链接了一个 _panic 属性,该属性在 panic 调用期间被链接。

func gopanic(e interface{}) {
   [...]
   var p _panic
   [...]
   d := gp._defer // 当前附加的 defer 函数
   [...]
   d._panic = (*_panic)(noescape(unsafe.Pointer(&p)))
   [...]
}

确实,在发生 panic 的情况下,调用延迟函数之前会调用 gopanic 方法:

0x0067 00103 (main.go:21)   CALL   runtime.gopanic(SB)
0x006c 00108 (main.go:21) UNDEF
0x006e 00110 (main.go:16) XCHGL AX, AX
0x006f 00111 (main.go:16) CALL runtime.deferreturn(SB)

这里是一个 recover 函数利用命名返回参数的例子:

func main() {
   fmt.Printf("error from err1: %v\n", err1())
   fmt.Printf("error from err2: %v\n", err2())
}

func err1() error {
   var err error

   defer func() {
      if r := recover(); r != nil {
         err = errors.New("recovered")
      }
   }()
   panic(`foo`)

   return err
}

func err2() (err error) {
   defer func() {
      if r := recover(); r != nil {
         err = errors.New("recovered")
      }
   }()
   panic(`foo`)

   return err
}
error from err1: <nil>
error from err2: recovered

两者的结合是我们可以正常使用 recover 函数将我们希望的 error 返回给调用方。作为这篇关于延迟函数的文章的总结,让我们来看看延迟函数的提升。

性能提升

Go 1.8[8]是提升 defer 的最近的一个版本(译者注:目前 Go 1.14 才是提升 defer 性能的最近的一个版本),我们可以通过运行 Go 的基准测试来看到这些提升(在 1.7 和 1.8 之间进行对比):

name         old time/op  new time/op  delta
Defer-4      99.0ns ± 9%  52.4ns ± 5%  -47.04%  (p=0.000 n=9+10)
Defer10-4    90.6ns ± 13%  45.0ns ± 3%  -50.37%  (p=0.000 n=10+10)

这样的提升得益于这个提升分配方式的 CL [9],避免了栈的增长。

不带参数的 defer 语句避免内存拷贝也是一个优化。下面是带参数和不带参数的延迟函数的基准测试:

name     old time/op  new time/op  delta
Defer-4  51.3ns ± 3%  45.8ns ± 1%  -10.72%  (p=0.000 n=10+10)

由于第二个优化,现在速度也提高了 10%。


via: https://medium.com/a-journey-with-go/go-how-does-defer-statement-work-1a9492689b6e

作者:Vincent Blanchon[10]译者:dust347[11]校对:@unknwon[12]

本文由 GCTT[13] 原创编译,Go 中文网[14] 荣誉推出

参考资料

[1]

defer 语句: https://golang.org/ref/spec#Defer_statements

[2]

Golang 规范: https://golang.org/ref/spec#Defer_statements

[3]

汇编: https://golang.org/doc/asm

[4]

命名返回参数: https://golang.org/ref/spec#Function_types

[5]

匿名函数( function literal ): https://golang.org/ref/spec#Function_literals

[6]

命名返回参数: https://golang.org/ref/spec#Function_types

[7]

defer, panic 和 recover: https://blog.golang.org/defer-panic-and-recover

[8]

Go 1.8: https://golang.org/doc/go1.8#defer

[9]

这个提升分配方式的 CL : https://go-review.googlesource.com/c/go/+/29656/

[10]

Vincent Blanchon: https://medium.com/@blanchon.vincent

[11]

dust347: https://github.com/dust347

[12]

@unknwon: https://github.com/unknwon

[13]

GCTT: https://github.com/studygolang/GCTT

[14]

Go 中文网: https://studygolang.com/



推荐阅读


学习交流 Go 语言,扫码回复「进群」即可


站长 polarisxu

自己的原创文章

不限于 Go 技术

职场和创业经验


Go语言中文网

每天为你

分享 Go 知识

Go爱好者值得关注




浏览 28
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报