Golang 的 defer
是什么?通俗来讲就是延迟调用。defer 会在当前函数返回之前执行 defer
注册的函数。比如 defer func_x( )
这样语句会让你注册一个函数变量到 defer 的全局链表中,在 defer 语句所在的函数退出之前调用。
笔者使用一段时间 Golang 之后,对 Golang defer 的理解认为作用有两点:
- panic 场景依然会被调用:这个是重要的一个特性,通常能简化我们的代码,确保无论任何场景,defer 的函数一定调用,通常用在锁或者资源的释放场景较多;
- 配套的两个行为代码可以放在最近的位置:创建&释放、加锁&放锁、前置&后置,使得代码更易读,编程体验优秀。最近的地方是哪里?下一行;
先看下 defer 的以下几个特性:
defer 的特性
我们先深入的剖析下 defer 具有的特性,知其然也。这些特性是需要我们记住的特点,才能更好的理解 defer
使用的场景。
延迟调用
package main
func main() {
defer println("--- defer ---")
println("--- end ---")
}
defer
会在 main 函数 return 之前时候调用。核心要点:
- 延迟调用:
defer
语句本身虽然是main
的第一行,但是 fmt.Println 是先打印的; defer
关键字一定是处于函数上下文:defer
必须放在函数内部;
LIFO
一个函数中有多个 defer 调用怎么办? 压栈式执行,后入先出。
package main
import (
"strconv"
)
func main() {
for i := 1; i <= 6; i++ {
defer println("defer -->" + strconv.Itoa(i))
}
println("--- end ---")
}
压栈式执行,也就是说先注册的函数后调用。如上,我们注册的顺序式 1,2,3,4,5,6,最后打印 “— end —",所以执行的结果自然是反着来的,程序输出:
--- end ---
defer -->6
defer -->5
defer -->4
defer -->3
defer -->2
defer -->1
作用域
要点:defer
和函数绑定。 两个理解,defer
只会和 defer
语句所在的特定函数绑定在一起,作用域也只在这个函数。 从语法上来讲,defer
语句也一定要在函数内,否则会报告语法错误。
package main
func main() {
func() {
defer println("--- defer ---")
}()
println("--- ending ---")
}
如上,defer
处于一个匿名函数中,就 main 函数本身来讲,匿名函数 fun(){}()
先调用且返回,然后再调用 println("--- ending ---")
,所以程序输出自然是:
--- defer ---
--- ending ---
异常场景
这个是个非常重要的特性:panic
也能执行。Golang 不鼓励异常的编程模式,但是却也留了 panic-recover
这个异常和捕捉异常的机制。所以 defer
机制就显得尤为重要,甚至可以说是必不可少的。因为你没有一个无视异常,永保调用的 defer
机制,很有可能就会发生各种资源泄露,死锁等场景。为什么?因为发生了 panic
却不代表进程一定会挂掉,很有可能被外层 recover
住。
package main
func main() {
defer func() {
if e := recover(); e != nil {
println("--- defer ---")
}
}()
panic("throw panic")
}
如上,main
函数注册一个 defer ,且稍后主动触发 panic
,main
函数退出之际就会调用 defer
注册的匿名函数。再提一点,这里其实有两个要点:
defer
在panic
异常场景也能确保调用;recover
必须和defer
结合才有意义;
使用姿势
并发同步
以下的例子对两个并发的协程做了下同步控制,常规操作。
var wg sync.WaitGroup
for i := 0; i < 2; i++ {
wg.Add(1)
go func() {
defer wg.Done()
// 程序逻辑
}()
}
wg.Wait()
锁场景
加锁解锁必须配套,在 Golang 有了 defer
之后,你就可以写了 lock
之后,立马就写 unlock
,这样就永远不会忘了。
mu.RLock()
defer mu.RUnlock()
但是请注意,lock
以下的代码都会在锁内。所以下面的代码要足够精简和快速才行,如果说下面的逻辑很复杂,那么可能就需要手动控制 unlock
防止的位置了。
资源释放
某些资源是临时创建的,作用域只存在于现场函数中,用完之后需要销毁,这种场景也适用 defer
来释放。释放就在创建的下一行,这是个非常好的编程体验,这种编程方式能极大的避免资源泄漏。因为写了创建立马就可以写释放了,再也不会忘记了。
// new 一个客户端 client;
cli, err := clientv3.New(clientv3.Config{Endpoints: endpoints})
if err != nil {
log.Fatal(err)
}
// 释放该 client ,也就是说该 client 的声明周期就只在该函数中;
defer cli.Close()
panic-recover
recover 必须和 defer 结合才行,使用姿势一般如下:
defer func() {
if v := recover(); v != nil {
_ = fmt.Errorf("PANIC=%v", v)
}
}()
总结
defer
其实并不是 Golang 独创,是多种高级语言的共同选择;defer
最重要的一个特点就是无视异常可执行,这个是 Golang 在提供了panic-recover
机制之后必须做的补偿机制;defer
的作用域存在于函数,defer
也只有和函数结合才有意义;defer
允许你把配套的两个行为代码放在最近相邻的两行,比如创建&释放、加锁&放锁、前置&后置,使得代码更易读,编程体验优秀;
本篇从 defer
的使用姿势入手,了解 defer
的特性,让大家知其然也。后续会从源码和实现的角度出发,梳理下 defer
,然后知其所以然也。
坚持思考,方向比努力更重要。关注公众号:奇伢云存储,获取更多干货。