「这是我参与2022首次更文挑战的第19天,活动详情查看:2022首次更文挑战」。
前言
defer有一些美丽的性质,每次defer语句执行的时候,会把函数“压栈”,函数参数会被拷贝下来。在函数退出的时候,会根据后进先出的性质,逐个执行之前的defer。
本文主要聊一下如何利用这些性质,以及这些性质的底层原理
举个例子
func mergeFile() error {
t, _ := os.Open("file1+2.txt")
f, _ := os.Open("file1.txt")
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close file1.txt err %v\n", err)
}
}(f)
}
//将f的内容写到t中
f, _ = os.Open("file2.txt")
if f != nil {
defer func(f io.Closer) {
if err := f.Close(); err != nil {
fmt.Printf("defer close file2.txt err %v\n", err)
}
}(f)
}
//将f的内容写到t中
return nil
}
由于defer运行时会将参数一起存入栈中,因此即使两个文件描述符都是f,那defer回收资源的时候也会回收全部所需回收的资源。不会因此造成内存泄露等问题。
此处有两个需要注意的地方,如果传递过去的是一个指针,那么defer后面对该变量的改动也会对这个defer造成影响(因为栈中存的也是指针嘛)。同时需要注意如果要使用该变量的函数,需要判断该变量是否为空,否则可能会造成panic。
原理
最开始defer的内容其实都是存储在堆中,但是性能上有些差。因为defer在被执行时需要将参数放入栈中,到那时还得从堆中移动到栈一次,很麻烦。
出于成本考虑,Go1.13在大部分情况下都将defer放入栈中。(小部分情况是for循环等动态调用的情况)
至于如何在堆和栈的存储,我们放在下面一片文章具体描述。
那么如何保证后进先出呢?defer会生成一个_defer结构体,然后将自己存储在链表中,
通过这种方式保证后进先出。