Go的defer使用心得





0 概述

Golang 的 defer 是什么?通俗来讲就是延迟调用。defer 会在当前函数返回之前执行 defer 注册的函数。比如 defer func_x( ) 这样语句会让你注册一个函数变量到 defer 的全局链表中,在 defer 语句所在的函数退出之前调用。

笔者使用一段时间 Golang 之后,对 Golang defer 的理解认为作用有两点:

  • panic 场景依然会被调用:这个是重要的一个特性,通常能简化我们的代码,确保无论任何场景,defer 的函数一定调用,通常用在锁或者资源的释放场景较多;

  • 配套的两个行为代码可以放在最近的位置:创建&释放、加锁&放锁、前置&后置,使得代码更易读,编程体验优秀。最近的地方是哪里?下一行;




1 defer 的特性

1.1 延迟调用

package main

func main() {
    
    
	defer println("--- defer ---")
	println("--- end ---")
}

程序输出:

--- end ---
--- defer ---

defer 会在 main 函数 return 之前时候调用。核心要点:

  • 延迟调用:defer 语句本身虽然是 main 的第一行,但是后打印的;
  • defer 关键字一定是处于函数上下文:defer 必须放在函数内部

1.2 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

1.3 作用域

要点:defer 和函数绑定。 两个理解:

  • defer 只会和 defer 语句所在的特定函数绑定在一起,作用域也只在这个函数。
  • 从语法上来讲,defer 语句也一定要在函数内,否则会报告语法错误。
package main

func main() {
    
    
 func() {
    
    
  defer println("--- defer ---")
 }()
 println("--- ending ---")
}

如上,defer 处于一个匿名函数中,就 main 函数本身来讲,匿名函数 fun(){}() 先调用且返回,然后再调用 println("— ending —") ,所以程序输出自然是:

--- defer ---
--- ending ---

1.4 异常场景

这个是个非常重要的特性:函数中出现异常,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 结合才有意义;




2 使用场景

2.1 并发同步

以下的例子对两个并发的协程做了下同步控制,常规操作。

var wg sync.WaitGroup

for i := 0; i < 2; i++ {
    
    
    wg.Add(1)
    go func() {
    
    
        defer wg.Done()
        // 程序逻辑
    }()
}
wg.Wait()

2.2 锁场景

加锁解锁必须配套,在 Golang 有了 defer 之后,你就可以写了 lock 之后,立马就写 unlock ,这样就永远不会忘了。

 mu.RLock()
 defer mu.RUnlock()

但是请注意,lock 以下的代码到函数结束都会在锁内。所以下面的代码要足够精简和快速才行,如果说下面的逻辑很复杂,那么可能就需要手动控制 unlock 防止的位置了。

2.3 资源释放

某些资源是临时创建的,作用域只存在于现场函数中,用完之后需要销毁,这种场景也适用 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)
	  }
 }()




3 关于defer慎入的坑

3.1 defer入栈时的值拷贝问题

说明:
1.当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈),
2.当函数执行完毕后,再从defer栈,按照先入后出的方式出栈,执行
3.defer将语句入栈时,也会将语句相关的值进行拷贝,同时入栈
注意:此操作是复制了一份值的副本而不是引用

测试代码如下:

func sum(n1 int, n2 int) int {
    
    
	// defer 会在该函数执行完成退出时执行
	defer fmt.Println("defer n1=", n1) // n1 = 10
	defer fmt.Println("defer n2=", n2) // n2 = 20

	n1++           // n1 = 11
	n2++           // n2 = 21
	res := n1 + n2 // res = 32
	fmt.Println("sum n1=", n1)
	fmt.Println("sum n2=", n2)
	return res
}

func main() {
    
    
	_ = sum(10, 20)
}

程序输出:

sum n1= 11
sum n2= 21
defer n2= 20
defer n1= 10

心得:
之前写过程序,函数执行完成后,在 defer 压入栈的函数中进行消息推送:

func(){
    
    
	// 定义 alarmInfoMap, alarmStg, alarmClassMap ....
	defer exitRoutine(alarmInfoMap, alarmStg, alarmClassMap)
	// 给 alarmInfoMap, alarmStg, alarmClassMap 赋值等操作 ....
}

defer 函数传参是推送消息体,结果消息总是为空,经过调试才发现,执行defer 之前没有消息,消息处理是在defer 函数之后。
解决方法,传参的时候可以传入地址的引用,示例如下:

//退出协程时执行
defer exitRoutine(&alarmInfoMap, &alarmStg, &alarmClassMap)

想要学习更多Go语言语法和Go语言在工作中的常用知识点可以参考我的笔记源码 https://github.com/qiuyunzhao/go_basis

猜你喜欢

转载自blog.csdn.net/QiuHaoqian/article/details/106234130