Go语言中defer的一些坑

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Go语言中defer的一些坑相关的知识,希望对你有一定的参考价值。


defer语句是Go中一个非常有用的特性,可以将一个方法延迟到包裹该方法的方法返回时执行,在实际应用中,defer语句可以充当其他语言中try…catch…的角色,也可以用来处理关闭文件句柄等收尾操作。

defer触发时机

A “defer” statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking.

Go官方文档中对defer的执行时机做了阐述,分别是。

  • 包裹defer的函数返回时
  • 包裹defer的函数执行到末尾时
  • 所在的goroutine发生panic时

defer执行顺序

当一个方法中有多个defer时, defer会将要延迟执行的方法“压栈”,当defer被触发时,将所有“压栈”的方法“出栈”并执行。所以defer的执行顺序是LIFO的。

所以下面这段代码的输出不是1 2 3,而是3 2 1。

func stackingDefers() 
defer func()
fmt.Println("1")
()
defer func()
fmt.Println("2")
()
defer func()
fmt.Println("3")
()

坑1:defer在匿名返回值和命名返回值函数中的不同表现

先看下面两个方法执行的结果。

func returnValues() int 
var result int
defer func()
result++
fmt.Println("defer")
()
return result


func namedReturnValues() (result int)
defer func()
result++
fmt.Println("defer")
()
return result

上面的方法会输出0,下面的方法输出1。上面的方法使用了匿名返回值,下面的使用了命名返回值,除此之外其他的逻辑均相同,为什么输出的结果会有区别呢?

要搞清这个问题首先需要了解defer的执行逻辑,文档中说defer语句在方法返回“时”触发,也就是说return和defer是“同时”执行的。以匿名返回值方法举例,过程如下。

  • 将result赋值给返回值(可以理解成Go自动创建了一个返回值retValue,相当于执行retValue = result)
  • 然后检查是否有defer,如果有则执行
  • 返回刚才创建的返回值(retValue)

在这种情况下,defer中的修改是对result执行的,而不是retValue,所以defer返回的依然是retValue。在命名返回值方法中,由于返回值在方法定义时已经被定义,所以没有创建retValue的过程,result就是retValue,defer对于result的修改也会被直接返回。

坑2:在for循环中使用defer可能导致的性能问题

看下面的代码

func deferInLoops() 
for i := 0; i < 100; i++
f, _ := os.Open("/etc/hosts")
defer f.Close()

defer在紧邻创建资源的语句后声明,看上去逻辑没有什么问题。但是和直接调用相比,defer的执行存在着额外的开销,例如defer会对其后需要的参数进行内存拷贝,还需要对defer结构进行压栈出栈操作。所以在循环中定义defer可能导致大量的资源开销,在本例中,可以将f.Close()语句前的defer去掉,来减少大量defer导致的额外资源消耗。

坑3:判断执行没有err之后,再defer释放资源

一些获取资源的操作可能会返回err参数,我们可以选择忽略返回的err参数,但是如果要使用defer进行延迟释放的的话,需要在使用defer之前先判断是否存在err,如果资源没有获取成功,即没有必要也不应该再对资源执行释放操作。如果不判断获取资源是否成功就执行释放操作的话,还有可能导致释放方法执行错误。

正确写法如下。

resp, err := http.Get(url)
// 先判断操作是否成功
if err != nil
return err

// 如果操作成功,再进行Close操作
defer resp.Body.Close()

坑4:调用os.Exit时defer不会被执行

当发生panic时,所在goroutine的所有defer会被执行,但是当调用os.Exit()方法退出程序时,defer并不会被执行。

func deferExit() 
defer func()
fmt.Println("defer")
()
os.Exit(0)

上面的defer并不会输出。

作用:defer延迟函数,释放资源,收尾工作;如释放锁,关闭文件,关闭链接;捕获panic;

避坑指南:defer函数紧跟在资源打开后面,否则defer可能得不到执行,导致内存泄露。

多个 defer 调用顺序是 LIFO(后入先出),defer后的操作可以理解为压入栈中

解析:函数的 return 语句并不是原子级的,实际上 return 语句只代理汇编指令 ret。defer 语句是在返回前执行,所以返回过程是:「设置返回值—>执行defer—>ret」。defer可以修改函数最终返回值,修改时机:有名返回值或者函数返回指针 参考:Go高阶指南07,一文搞懂 defer 实现原理)


以上是关于Go语言中defer的一些坑的主要内容,如果未能解决你的问题,请参考以下文章

面试Go 被defer的几个盲区坑了

golang中defer的正确使用方式(源自深入解析go)

Go语言基础:闭包_defer_recover

Go语言基础:闭包_defer_recover

Go语言中的defer

Go-关键字deferpanicrecover详解