go语言defer系列(二)

196 阅读2分钟

「这是我参与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结构体,然后将自己存储在链表中,

K14MXPO4HAM1BB%1)JI5F.png

通过这种方式保证后进先出。