defer 是咱们常常会应用的一个关键字,它会在以后函数返回前执行传入的函数,罕用于敞开文件描述符、敞开数据库连贯以及解锁资源。
应用场景
开释资源
这是 defer 最常见的用法,包含开释互斥锁、敞开文件句柄、敞开网络连接、敞开管道和进行定时器等,如:
m.mutex.Lock()
defer m.mutex.Unlock()
异样解决
defer 第二个重要用处就是解决异样,与 recover 搭配一起解决 panic,让程序从异样中复原。例如 gin 框架中 recovery 中间件的源码:
return func(c *Context) {defer func() {if err := recover(); err != nil {
// Check for a broken connection, as it is not really a
// condition that warrants a panic stack trace.
var brokenPipe bool
if ne, ok := err.(*net.OpError); ok {if se, ok := ne.Err.(*os.SyscallError); ok {if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {brokenPipe = true}
}
}
// ...
批改命名返回值
// $GOROOT/src/fmt/scan.go
func (s *ss) Token(skipSpace bool, f func(rune) bool) (tok []byte, err error) {defer func() {if e := recover(); e != nil {if se, ok := e.(scanError); ok {err = se.err} else {panic(e)
}
}
}()
...
}
打印调试信息
// $GOROOT/src/net/conf.go
func (c *conf) hostLookupOrder(r *Resolver, hostname string) (ret hostLookupOrder) {
if c.dnsDebugLevel > 1 {defer func() {print("go package net: hostLookupOrder(", hostname, ") =", ret.String(), "\n")
}()}
...
}
行为规定
defer 的语法很简略,不过衍生出的用法很多,有时让人蛊惑,在这里咱们总结一下 defer 的几个根本应用规定。
同一函数外部不同 defer 关键字前面的函数是逆序执行的
前面咱们会探讨,defer 关键字前面的提早函数须要注册到一个 deferred 函数栈中(实质上是一个链表),因而遵循栈的后进先出规定,多个 defer 前面的函数是逆序执行的。
defer 关键字前面的函数参数是在 defer 关键字呈现时预计算的
defer 关键字前面的函数是在注册到 deferred 函数栈的时候进行求值的。上面看一个例子:
func test1() {
for i := 0; i <= 3; i++ {defer func(n int) {fmt.Println(n)
}(i)
}
}
func test2() {
for i := 0; i <= 3; i++ {defer func() {fmt.Println(i)
}()}
}
func main() {test1()
test2()}
在 test1 中,defer 前面接的是一个带有一个参数的匿名函数。每当 defer 将匿名函数注册到 deferred 函数栈的时候,都会对该匿名函数的参数进行求值。因而,deferred 函数栈中匿名函数的参数顺次是 0,1,2,3,打印进去的后果就是 3,2,1,0。
在 test2 中,defer 前面接的是一个不带参数的匿名函数。当代码执行的时候,deferred 栈中弹出的函数会以闭包的形式拜访内部变量 i,而此时 i 的值曾经变为了 4,因而打印后果为 4,4,4,4。
实现原理
数据结构
// src/runtime/runtime2.go
type _defer struct {
siz int32
started bool
heap bool
openDefer bool
sp uintptr
pc uintptr
fn *funcval
_panic *_panic
link *_defer
}
- siz 是参数和后果的内存大小;
- heap 示意该构造体是否存于堆中;
- sp 和 pc 别离代表栈指针和调用方的程序计数器;
- fn 是 defer 关键字中传入的函数;
- _panic 是触发提早调用的构造体,可能为空;
- openDefer 示意以后 defer 是否通过凋谢编码的优化。
能够看出,每个 _defer 实例实际上是对一个函数的封装,领有执行函数的必要信息,如栈指针等。多个 _defer 实例应用指针 link 连接起来造成一个单向链表,保留到以后 goroutine 的数据结构中,待以后函数执行完结再一一取出执行。
type g struct {
// ...
_defer *_defer
// ...
}
执行机制
在两头代码生成阶段会处理程序中的 defer,依据不同的条件,会有三种不同的机制来解决该关键字:堆调配、栈调配和凋谢编码。晚期的 Go 语言会在堆上调配 _defer 构造体,不过该实现的性能较差,在 1.13 版本中引入栈上调配的构造体,缩小了 30% 的额定开销,而后在 1.14 中引入了基于凋谢编码的 defer,使得性能大幅度晋升。
堆调配
堆调配是默认的兜底计划,采纳本计划时,在编译期间会将 defer 关键字转换成 runtime.deferproc 函数,并且为所有调用 defer 的函数开端插入 runtime.deferreturn 函数。简而言之,就是 deferproc 生成 _defer 构造体并插入链表,deferreturn 取出 _defer 并执行。
来简略看一下源码:
func deferproc(siz int32, fn *funcval) {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.fn = fn
d.pc = callerpc
d.sp = sp
switch siz {
case 0:
case sys.PtrSize:
*(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
default:
memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
}
return0()}
deferproc 会为 defer 创立一个新的 _defer 构造体、设置它的函数指针 fn、程序计数器 pc 和栈指针 sp 并将相干的参数拷贝到相邻的内存空间中。
newdefer 的作用是获取 _defer 构造体,这里蕴含三种门路:
- 从调度器的提早调用缓存池 sched.deferpool 中取出构造体并将该构造体追加到以后 Goroutine 的缓存池中;
- 从 Goroutine 的提早调用缓存池 pp.deferpool 中取出构造体;
- 通过 runtime.mallocgc 在堆上创立一个新的构造体。
无论应用哪种形式,获取到 _defer 构造体之后,都会被追加到所在 Goroutine 的 _defer 链表的最后面。
栈调配
栈调配 defer 是为了进步堆调配 defer 的内存应用效率而引入的,当 defer 关键字在函数中最多执行一次时,编译器就会将 defer 编译成 deferprocStack 函数将 _defer 构造体调配到栈上。
在编译期间曾经创立了 _defer 构造体,所以 deferprocStack 只须要设置一些未初始化的字段,而后将栈上的 _defer 追加到链表上。
func deferprocStack(d *_defer) {gp := getg()
d.started = false
d.heap = false
d.openDefer = false
d.sp = getcallersp()
d.pc = getcallerpc()
d.framepc = 0
d.varp = 0
*(*uintptr)(unsafe.Pointer(&d._panic)) = 0
*(*uintptr)(unsafe.Pointer(&d.fd)) = 0
*(*uintptr)(unsafe.Pointer(&d.link)) = uintptr(unsafe.Pointer(gp._defer))
*(*uintptr)(unsafe.Pointer(&gp._defer)) = uintptr(unsafe.Pointer(d))
return0()}
凋谢编码
无论是堆调配 defer 还是栈调配 defer,编译器都只能把 defer 转换成相应的创立 _defer 构造体的函数,最初通过 deferreturn 函数取出构造体再执行。如果编译器不这么麻烦,间接把 defer 语句转换成相应的代码插入函数尾部,是不是就能够节俭很多步骤,进步存储效率和性能?凋谢编码应用的就是这种思路。
但并不是所有的状况下都能够应用凋谢编码方式,在一下场景下 defer 语句不能被解决成凋谢编码类型:
- 编译时禁用了编译器优化;
- defer 呈现在循环中;
- 单个函数中 defer 呈现了 8 个以上;
- 单个函数中 return 语句的个数乘以 defer 语句的个数超过了 15。