即使panic,也会执行defer
|
|
在使用数据库事务时,我们可以使用上面的代码在创建事务后就立刻调用 Rollback 保证事务一定会回滚。哪怕事务真的执行成功了,那么调用 tx.Commit() 之后再执行 tx.Rollback() 也不会影响已经提交的事务。
defer 关键字的调用时机以及多次调用 defer 时执行顺序是如何确定的;
defer 关键字使用传值的方式传递参数时会进行预计算,导致不符合预期的结果;
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 传入的函数不是在退出代码块的作用域时执行的,它只会在当前函数和方法返回之前被调用
defer 关键字会立刻拷贝函数中引用的外部参数
根据这个特点,使用defer func() {} 的方式避免
defer数据结构:
|
|
堆分配、栈分配和开放编码(Open coded)是处理 defer 关键字的三种方法,早期的 Go 语言会在堆上分配 runtime._defer 结构体,不过该实现的性能较差,Go 语言在 1.13 中引入栈上分配的结构体,减少了 30% 的额外开销1,并在 1.14 中引入了基于开放编码的 defer,使得该关键字的额外开销可以忽略不计2
Go 语言在 1.14 中通过开放编码(Open Coded)实现 defer 关键字,该设计使用代码内联优化 defer 关键的额外开销并引入函数数据 funcdata 管理 panic 的调用3,该优化可以将 defer 的调用开销从 1.13 版本的 ~35ns 降低至 ~6ns 左右:
defer 关键字的实现主要依靠编译器和运行时的协作,我们总结一下本节提到的三种机制:
堆上分配 · 1.1 ~ 1.12
编译期将 defer 关键字转换成 runtime.deferproc 并在调用 defer 关键字的函数返回之前插入 runtime.deferreturn;
运行时调用 runtime.deferproc 会将一个新的 runtime._defer 结构体追加到当前 Goroutine 的链表头;
运行时调用 runtime.deferreturn 会从 Goroutine 的链表中取出 runtime._defer 结构并依次执行;
栈上分配 · 1.13
当该关键字在函数体中最多执行一次时,编译期间的 cmd/compile/internal/gc.state.call 会将结构体分配到栈上并调用 runtime.deferprocStack;
开放编码 · 1.14 ~ 现在
编译期间判断 defer 关键字、return 语句的个数确定是否开启开放编码优化;
通过 deferBits 和 cmd/compile/internal/gc.openDeferInfo 存储 defer 关键字的相关信息;
如果 defer 关键字的执行可以在编译期间确定,会在函数返回前直接插入相应的代码,否则会由运行时的 runtime.deferreturn 处理;
我们在本节前面提到的两个现象在这里也可以解释清楚了:
后调用的 defer 函数会先执行:
后调用的 defer 函数会被追加到 Goroutine _defer 链表的最前面;
运行 runtime._defer 时是从前到后依次执行;
函数的参数会被预先计算;
调用 runtime.deferproc 函数创建新的延迟调用时就会立刻拷贝函数的参数,函数的参数不会等到真正执行时计算;