文章目录
1 | 后进先出
- 这个大家都差不多明白了,后进入的 defer 函数先执行
func main() {
defer func1()
defer func2()
defer func3()
return
}
- 执行顺序:func3 --> func2 --> func1
2 | 理解 defer 执行的时机
- return 函数并不是个「原子操作」,拥有 defer 函数的 return 其运行过程如下
- 先设置返回值
- 执行defer函数
- ret
3 | defer 函数的参数在语句出现时就已经确定了
- go 语言是值传递(就是传递的参数,在新函数中会复制一份)
- 因此 defer 语句出现时,其参数就复制了一份,后面对此参数的改变,不会影响 defer 已经复制的(除非传递的是指针)
func a() {
i := 0
defer fmt.Println(i) // 实际是 复制了一份 i,用于 defer 使用
i++ // 不会影响 defer 复制的
return
}
// 输出为: 0
4 | 运用学习
4.1 | 主函数拥有匿名返回值,返回字面量
func foo() int {
var i int
defer func() {
i++
}()
return 1
}
// 输出为: 1
// return 直接返回 字面量 1,defer 函数无法操作
4.2 | 主函数拥有匿名返回值,返回变量
func foo() int {
var i int
defer func(){
i++
}()
return i
}
// 输出为:0
// 第二条规则,return:先设置返回值 --> 执行 defer --> 结束 ret
// 首先设置返回值,因为是匿名返回值,那我们可以随意起个名 annoy
// 因此首先设置返回值。 annoy = i
// 执行 defer。 i++ (不会对 annoy 产生影响)
// 返回。 返回 annoy
4.3 | 主函数拥有具名返回值
func foo() (ret int) {
defer func(){
ret++
}()
return 0
}
// 输出为:1
// 第二条规则,return:先设置返回值 --> 执行 defer --> 结束 ret
// 首先设置返回值,因为是匿名返回值,那我们可以随意起个名 annoy
// 因此首先设置返回值。 ret = 0
// 执行 defer。 ret++ (不会对 annoy 产生影响)
// 返回。 返回 ret
// 有人会奇怪,之前说的不是 『defer 会进行参数复制,不影响吗?』,怎么又影响了?
// 因为这里 defer 没有参数,直接易用外部函数的 ret 变量(这也叫做 『闭包』)
5 | defer 的底层原理了解
5.1 | defer 的创建与执行
源码包中 src/runtime/panic.go 定义了两个方法,分别用于创建 defer 和执行 defer
deferproc(): 负责把 defer 函数处理成 _defer 实例,并存入 goroutine 中的链表
deferreturn(): 负责把 defer 从 goroutine 链表中的 defer 实例取出并执行
简单理解为:
- 编译器在编译阶段把 defer 语句替换成 函数 deferproc()
- 在函数 return() 前插入了函数 deferreturn()
- 在运行时,每次执行 deferproc() 都创建一个运行时 _defer 实例并存储
- 函数返回前执行 deferreturn() 依次取出 _defer 实例并执行
5.2 | defer 的性能优化
- heap-allocated:存储在堆上的 defer, Go 1.13 之前只有这种类型
- stack-allocated:存储在站上的 defer, Go 1.13 引入这种类型
- open-coded: 开放编码类型的 defer, Go 1.14 引入这种类型
- 引入了新类型后也没有废弃原类型,而是多种类型并存
分析
- heap-allocated
- defer 节点存储在堆上
- 痛点主要是频繁的堆内存分配及释放,性能稍差
- stack-allocated
- 编译器会直接在栈上预留 _defer 的存储空间,执行结束后不需要再释放内存
- 由于栈空间有限,并不能把所有的 defer 都存储在栈中,所以还需要保留堆 defer
- open-coded
- 无论是堆 defer 还是 栈 defer,编译器都只能把 defer 替换成相应的函数(deferproc 堆的,deferproStack 栈的),然后创建 _defer 节点存储起来,最后通过 deferreturn() 函数取出这些 _defer 节点再执行
- 开放编码的区别是:直接可以翻译成相应的执行代码并插入函数尾部,节省转换成 _defer 节点进行存储的代价
- 但并不是所有的 defer 语句都可以被编译器处理成开放编码类型,如以下就不能:
- 编译时禁用了编译器优化,即 -gcflags="-N -l"
- defer 出现在循环语句中
- 单个函数中 defer 出现了 8个以上,或者 return 语句的个数和 defer 语句的个数乘积超过了 15
6 | recover 函数一般配合来使用
-
recover() 函数成功处理异常后,无法再次回到本函数发生 panic 的位置继续执行
-
recover() 函数的调用必须要位于 defer 函数中,且不能出现在另一个嵌套函数中
-
recover() 函数可以消除本函数产生或收到的 panic,上游函数感知不到 panic 的发生
// recover() 位于 defer 中
func RecoverDemo1() {
defer func() {
if err := recover(); err != nil {
fmt.Println("A")
}
}()
panic(nil)
fmt.Println("B")
}
// recover() 函数必须直接位于 defer 函数中才有效,嵌套无效
func RecoverDemo1() {
defer func() {
func() { // recover 在 defer 的嵌套函数中 无效
if err := recover(); err != nil {
fmt.Println("A")
}
}()
}()
panic(nil)
fmt.Println("B")
}