作者:漠然粉蔷薇778 | 来源:互联网 | 2023-09-13 17:27
文章目录defer应用defer触发时机defer执行顺序预计算参数defer实现原理defer应用Go语言的defer会在当前函数或者方法返回之前执行传入的函数。它会经常被用于
defer应用 Go 语言的 defer
会在当前函数或者方法返回之前执行传入的函数。它会经常被用于关闭文件描述符、关闭数据库连接以及解锁资源。 比如解锁资源:
mu. Lock ( ) defer mu. Unlock ( )
我们在 Go 语言中使用 defer 时会遇到两个比较常见的问题,这里会介绍具体的场景并分析这两个现象背后的设计原理:
defer 关键字的调用时机以及多次调用 defer 时执行顺序是如何确定的; defer 关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果; defer触发时机 defer的触发时机主要有三个:
函数执行到函数体末端 函数执行return语句 当前协程panic defer执行顺序 直接用go程序演示:
func main ( ) { for i :&#61; 0 ; i < 5 ; i&#43;&#43; { defer fmt. Println ( i) } }
其输出为&#xff1a;
4 3 2 1 0
运行上述代码会倒序执行所有向 defer 关键字中传入的表达式&#xff0c;最后一次 defer 调用传入了 fmt.Println(4)
&#xff0c;所以会这段代码会优先打印 4。
我们可以通过下面这个简单例子强化对 defer 执行时机的理解&#xff1a;
func main ( ) { { defer fmt. Println ( "defer runs" ) fmt. Println ( "block ends" ) } fmt. Println ( "main ends" ) }
$ go run main.go block ends main ends defer runs
defer 传入的函数不是在退出代码块的作用域时执行的&#xff0c;它会在当前函数和方法返回之前被调用。
预计算参数 假设我们想要计算 main 函数运行的时间&#xff0c;可能会写出以下的代码&#xff1a;
func main ( ) { startedAt :&#61; time. Now ( ) defer fmt. Println ( time. Since ( startedAt) ) time. Sleep ( time. Second) }
$ go run main.go 0s
我们理想的输出结果应该是1s
&#xff0c;但是上述代码的运行结果并不符合我们的预期。
调用 defer 关键字会立刻对函数中引用的外部参数进行拷贝 &#xff0c;所以 time.Since(startedAt) 的结果不是在 main 函数退出之前计算的&#xff0c;而是在 defer 关键字调用时计算的&#xff0c;最终导致上述代码输出 0s。
想要解决这个问题的方法非常简单&#xff0c;我们只需要向 defer 关键字传入匿名函数&#xff1a;
func main ( ) { startedAt :&#61; time. Now ( ) defer func ( ) { fmt. Println ( time. Since ( startedAt) ) } ( ) time. Sleep ( time. Second) }
$ go run main.go 1s
虽然调用 defer 关键字时也使用值传递&#xff0c;但是因为拷贝的是函数指针 &#xff0c;所以 time.Since(startedAt)
会在 main 函数返回前被调用并打印出符合预期的结果。
defer实现原理 首先来了解一下 defer 关键字在 Go 语言源代码中对应的数据结构&#xff0c;defer数据结构的源码在src/runtime/runtime2.go
中定义&#xff1a;
type _defer struct { siz int32 started bool sp uintptr pc uintptr fn * funcval_panic * _paniclink * _defer}
我们简单介绍一下 runtime._defer 结构体中的几个字段&#xff1a;
siz
是参数和结果的内存大小&#xff1b;sp
和 pc
分别代表栈指针和调用方的程序计数器&#xff1b;fn
是 defer 关键字中传入的函数&#xff1b;_panic
是触发延迟调用的结构体&#xff0c;可能为空&#xff1b;link
&#xff1a;注意到&#xff1a;*_defer
&#xff0c;说明了这个数据结构实际上是一个链表。 除了上述的这些字段之外&#xff0c;runtime._defer
中还包含一些垃圾回收机制使用的字段&#xff0c;这里为了减少理解的成本就都省去了。
中间代码生成阶段执行的被 cmd/compile/internal/gc.state.stmt
函数会处理 defer 关键字。从下面截取的这段代码中&#xff0c;我们会发现编译器调用了 cmd/compile/internal/gc.state.call
函数&#xff0c;这表示 defer 在编译器看来也是函数调用&#xff1a;
func ( s * state) stmt ( n * Node) { switch n. Op { case ODEFER: s. call ( n. Left, callDefer) } }
对于defer关键字&#xff0c;主要有3个函数&#xff1a;
deferproc
。在每遇到一个defer关键字时&#xff0c;实际上都会转换为deferproc函数&#xff0c;deferproc函数的作用是将defer函数存入链表中 。deferreturn
。在return指令前调用&#xff0c;从链表中取出defer函数并执行 。deferprocStack
。go1.13后对defer做的优化&#xff0c;通过利用栈空间提高效率。 编译期&#xff1b; 将 defer 关键字被转换 runtime.deferproc
&#xff1b; 在调用 defer 关键字的函数返回之前插入 runtime.deferreturn
&#xff1b; 运行时&#xff1a; runtime.deferproc
会将一个新的 runtime._defer
结构体追加到当前 Goroutine 的链表头&#xff1b;runtime.deferreturn
会从 Goroutine 的链表中取出 runtime._defer
结构并依次执行&#xff1b; 如果要了解详情&#xff0c;参考&#xff1a;理解Go语言defer关键字的原理