Featured image of post Go语言——延迟函数defer的使用

Go语言——延迟函数defer的使用

Go语言延迟函数defer的使用介绍和一些问题解释

延迟函数(defer)

  延迟函数,顾名思义,是在之后执行的函数,那么具体怎么个之后法呢?在函数调用的过程中,可以理解为调用函数的过程是一个链表,a->b->c,整个调用过程是先a再b再c,而延迟函数就是在调用a的程序过程中写入一个延迟执行,在a函数return之后添加一个函数调用。因此,defer的常用功能为释放资源、关闭连接、捕获错误等,这是一种语言保护机制。举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func Open() {
	f, err := os.Open("")
	if err != nil {
		return
	} // 如果此处Open函数没有问题,继续向下
	s, err := os.Create("")
	if err != nil {
		return // 如果此处发生error,那么直接return,后面的两个close都不执行
	}
	f.Close()
	s.Close()
	return 
}

  上面这段代码中如果Open函数正常执行,但是Create函数错了,返回错误时下面的close函数没有办法执行,就会导致f没有关闭,但是我们又不能直接写在上面,因为还没对f进行操作就关闭了文件,所以这里就要使用延迟函数了。

1
2
3
4
5
6
7
func Open() {
	f, err := os.Open("")
	if err != nil {
		return
	}
	defer f.Close() // 注意这里还有一个坑,下面再介绍
}

  接下来我们就介绍defer的使用:

defer的执行参数

  defer的执行参数在defer写入的位置就已经确定好了,也就是说,defer并不只是单单在return之后执行,而是在准备延迟调用的时候,就已经把参数一并压入堆栈中等待执行。

1
2
3
4
5
6
7
func main() {
	x := 5
	defer fmt.Println(x) // 这个时候已经将x=5传参给print函数了
	x++
	fmt.Println(x)
	// 结果打印为6 5 (注意正常是换行打印,为了看起来方便就这么写了,后面也一样)
}

  思考一下这样写结果呢?

1
2
3
4
5
6
7
8
9
func main() {
	x := 5
	defer func() {
		fmt.Println(x) // 这里相当于一个闭包,defer执行的是一个func(),但是func()不需要任何参数,而是去执行打印x,这时候x就打印出为6了,请区分和下面的匿名函数的问题
	}()
	x++
	fmt.Println(x)
	// 结果打印为6 6
}

defer的执行顺序

  在知道了defer的执行参数后就需要知道defer的执行顺序,如果有多条defer语句的话,根据压入顺序,最后是逆序执行的。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
func main() {
	x := 5
	defer func() { // 最后执行
		fmt.Println(x) 
	}()
	defer func() { // 再执行
		fmt.Println("ZonzeeLi") 
		x++
	}()
	defer func() { // 先执行
		fmt.Println("你好") 
	}()
	// 结果打印为 你好 ZonzeeLi 6
}

defer对匿名返回值的返回处理

  我们在自己写一个func()的时候经常会写返回值,但是返回值时分命名和匿名两种,这在defer中又有区别了,我们看下面代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
func f1() int {
	x := 5 
	defer func() {
		x++
		// 在这个地方加一个打印x,看看结果是多少?
	}()
	return x
	// 这个函数返回5
}

func f2() (x int) { // 如果把这个x换成y的话,返回结果是多少?
	//x = 0
	defer func() {
		x++
	}()
	return 5
	// 这个函数返回6
}

func f4() (x int) { // 这个返回是多少?
	defer func(x int) {
		x++
		// 这里加一个打印x,打印的是多少?
	}(x)
	return 50
	// 这个结果返回为50,请自行思考,解释参考上面的defer执行参数
}

  f1中使用的是匿名返回值,f2中使用的是命名返回值,f1的匿名返回值,相当于返回x的一个副本给f1的return,所以对x进行x++操作没有对return进行影响,而有命名返回值则不同,defer的作用域始终是在这个函数内的,所以使用的返回值就是x。

defer panic recover的配合使用

1
2
3
4
5
6
7
8
func test() {
	panic(1)
}

func main() {
	test() // 结果为panic: 1, 不会打印2
	fmt.Println(2)
}

  我们知道如果函数panic了,就直接终止运行了,后面的语句都不会执行,但是如果我们想要执行的话就需要recover,这时候defer的延迟使用就非常有帮助了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func test() {
	defer func() {
		if err := recover(); err != nil {
			fmt.Println("捕获成功啦!")
		}
	}()
	panic(1)
}

func main() {
	test()
	fmt.Println(2) // 结果为 捕获成功啦!2
}

  另外这里要注意的是,如果连续调用panic,仅最后一个会被recover捕获。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func main() {
	defer func() {
		for {
			if err := recover(); err != nil {
				log.Println(err)
			} else {
				log.Fatalln("fatal")
			}
		}
	}()

	defer func() {
		panic("you are dead")
	}()

	panic("i am dead")
	
	// 结果为
	// 2022/03/08 15:58:08 you are dead
	// 2022/03/08 15:58:08 fatal
}

  既然说到这,我就补充一下,defer中直接调用recover是无效的,举例如下:

1
2
3
4
5
6
func main() {
	defer log.Println(recover()) // 无效
	defer recover() // 无效
	
	panic("i am dead")
}

常见的defer面试题和坑

  以上将defer的几个用法介绍完了,下面我总结了在一些博主的文章中看到的问题和平常遇见的坑:

问题一,一道面试题,出自李文周的博客

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
func calc(index string, a, b int) int {
	ret := a + b
	fmt.Println(index, a, b, ret)
	return ret
}

func main() {
	x := 1
	y := 2
	defer calc("AA", x, calc("A", x, y)) // 先执行了calc("A", x, y),然后再压入defer执行语句和defer需要的参数
	x = 10
	defer calc("BB", x, calc("B", x, y))
	y = 20
	// 结果打印为
	// A 1 2 3
	// B 10 2 12
	// BB 10 12 22
	// AA 1 3 4
}

  上面的这个解释还是主要由defer的执行参数有关系,首先再执行defer calc("AA", x, calc("A", x, y)),他的三个参数就已经确定了,那么第三个参数是由calc("A", x, y)的返回值决定的,所以这个时候就会先执行这一条,然后将参数一同压入栈,将延迟函数压入函数调用,下面的defer calc("BB", x, calc("B", x, y))也同理。

问题二,for循环导致性能问题,出自《Go语言学习笔记》

1
2
3
4
5
6
7
func test() {
	...
	for i := 0;i < n;i ++ {
		f, _ := os.Open("")
		defer f.Close()
	}
}

  上面的代码其实没什么运行问题,只是defer的执行需要尽心很多压栈出栈以及内存使用的操作,会消耗很多资源,所以应该直接执行或者封装。

问题三,defer语句的位置,出自牛客网Go语言面试题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
func Open() {
	f, err := os.Open("")
	defer f.Close()
	if err != nil {
		...
		return 
	}
	// defer f.Close()
	...
}

  上面的代码写法有什么问题?也正常执行延迟Close了?问题在于defer语句的位置,如果写在判断err然后return之前,如果Open发生异常,那么f就是空指针,在return之后执行defer f.Close()就会报空指针错误,所以要在处理玩错误,return之后执行defer。

问题四,如下代码,当函数deferDemo()返回失败时,并不能destroy已create成功的资源,这一说法是否正确,出自牛客网Go语言面试题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
func deferDemo() error {
	err := createResource1()
	if err != nil {
		return ERR_CREATE_RESOURCE1_FAILED // 如果1失败,直接返回没有任何创建成功的资源
	}
	defer func() {
		if err != nil { 
			destroyResource1()
		}
	}()

	err = createResource2()
	if err != nil {
		return ERR_CREATE_RESOURCE2_FAILED // 如果2创建失败,那么执行上面的defer,destroy已经创建的1
	}
	defer func() {
		if err != nil {
			destroyResource2()
		}
	}()

	err = createResource3()
	if err != nil {
		return ERR_CREATE_RESOURCE3_FAILED // 同理
	}
	defer func() {
		if err != nil {
			destroyResource3()
		}
	}()
	
	return nil
}

  正确答案是错误,也就是如果返回失败,可以destroy已经create的资源,解释如下,如果返回成功,不需要讨论,如果返回失败,我们假设在createResource1()处失败,这时候2、3都没有进行创建,1也没有创建成功,直接返回,所以没有创建成功的资源,不需要destroy,如果在createResource2(),这时候err有了值,在返回2失败后,执行了上面1成功后压入的defer,破坏了1,下面的3也同理。

问题五,下面这段话是否正确,出自牛客网Go语言面试题

  当程序运行时,如果遇到引用空指针、下标越界或显式调用panic函数等情况,则先触发panic函数的执行,然后调用延迟函数。调用者继续传递panic,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数。如果一路在延迟函数中没有recover函数的调用,则会到达该协程的起点,该协程结束,然后终止其他所有协程,其他协程的终止过程也是重复发生:函数停止执行,调用延迟执行函数。这一说法是否正确。

  答案是错误的,上面的这段话需要注意的是一个地方,则先触发panic函数的执行,然后调用延迟函数,panic是需要等defer结束后才会向上传递,出现panic,会先按照defer的逆序执行顺序,执行完之后才会执行panic。有一段大佬的完整解释为:“当内置的panic()函数调用时,外围函数或方法的执行会立即终止。然后,任何延迟执行(defer)的函数或方法都会被调用,就像其外围函数正常返回一样。最后,调用返回到该外围函数的调用者,就像该外围调用函数或方法调用了panic()一样,因此该过程一直在调用栈中重复发生:函数停止执行,调用延迟执行函数等。当到达main()函数时不再有可以返回的调用者,因此这个过程会终止,并将包含传入原始panic()函数中的值的调用栈信息输出到os.Stderr”,可以举个例子:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
func test() {
	defer fmt.Println("test.1")
	defer fmt.Println("test.2")

	panic("i am dead")
}

func main() {
	defer func() {
		log.Println(recover())
	}()
	test()
	// 结果为
	// test.2
	// test.1
	// 2022/03/07 15:53:27 i am dead
}

问题六,思考结果,出自coding进阶公众号文章

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
func bar() (r int) {
	defer func() {
		r += 4 // 3. r += 4
		if recover() != nil { // 4. recover()捕获成功
			r += 8 // 5.r += 8 输出 13 
		}
	}()

	var f func()
	defer f() // 2.先执行这个,但是因为defer语句的位置,f()报错
	f = func() {
		r += 2
	}

	return 1 // 1.先给r进行赋值
}

func main() {
	println(bar()) //结果为13
}

  答案是13,你思考对了么?这道题的关键在于那个defer f(),此处因为defer语句的写法位置,因为var f func(),f()是空指针,而直接进行defer f(),在对f()赋值,所以在defer中会panic,这个就和上面说的问题三一样了,所以正确的执行顺序是,返回1给r,然后defer f()直接panic,再向上进行,r += 4然后再recover()捕获了panic,执行了r += 8

问题七,思考结果,出自coding进阶

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
func main() {
	defer func() {
		fmt.Print(recover()) // 4.同理捕获到了1
	}()
	defer func() {
		defer fmt.Print(recover()) // 2.先解析了参数,所以这里先执行了recover(),这时候还没有按照defer的逆序执行,所以先把2捕获到了
		defer panic(1)
		recover() // 3.有效,但是被截胡了
	}()
	defer recover() // 1.无效
	panic(2)
}

  结果是21,你回答对了么?我猜测多数人会回答nil1,还有人会回答nil,我这里分别解释一下,回答nil的是因为没理解recover的使用,recover必须配合defer的函数体直接调用使用,否则都会捕获无效,这个在上面都有介绍到,所以第一个defer(从下往上)是无效的,回答为nil1的是注意到了很多,但是最关键的在fmt.Print(recover())中,recover()作为参数被defer中的func调用,要先执行参数解析,所以2就会被先捕获到,而不是正常的先捕获经过头插法的最近panic,同理最上面的defer就捕获到了1。


  延迟函数defer的基础使用就差不多介绍这些,具体太细的,以及配合panic和recover使用的需要看底层和汇编才能理解的更透彻。

  Coding every day,let’s go!

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy