golang学习之四:闭包、defer

本文详细讲解了Go语言中的闭包概念,如何通过引用捕获外部变量,以及defer关键字的延迟执行特性,包括多个defer语句的执行顺序和如何与panic及recover配合使用。重点演示了从panic中恢复程序流程的方法。
摘要由CSDN通过智能技术生成

闭包

闭包以引用的方式捕捉外部变量

package main

import "fmt"

func main() {

	a := 10
	str := "狂歌痛饮空度日"
	func() {
		a = 14
		str = "飞扬跋扈为谁雄"
		fmt.Printf("闭包:a= %d, str = %s\n", a, str)
	}()// 函数调用

	fmt.Printf("外部:a= %d, str = %s\n", a, str)

}

控制台打印

闭包:a= 14, str = 飞扬跋扈为谁雄
外部:a= 14, str = 飞扬跋扈为谁雄
PS D:\vscode\code\demo1>

以上例子说明,闭包以引用的方式捕捉外部变量,闭包里更改了变量,实则是引用的方式,所以外面的变量也跟着改变了。

闭包变量与作用域

所谓闭包就是一个函数“捕获”了和它在同一作用域的其它常量和变量。这就意味着当闭包被调用的时候,不管在程序什么地方调用,闭包能够使用这些常量或者变量。它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还会存在。

下面我们来看两个例子去理解一下上面这句话

传统函数局部变量

package main

import "fmt"

func test1() int {
	// 函数被调用时,x才分配空间,才初始化为0
	var x int // int 类型没有被初始化,值为0
	x++
	return x * x // 函数调用完毕,x 自动释放
}

func main() {
	fmt.Println(test1())
	fmt.Println(test1())
	fmt.Println(test1())
	fmt.Println(test1())
	fmt.Println(test1())
}

控制台

PS D:\vscode\code\demo1> go run bibao2.go
1
1
1
1
1

以上代码没有什么好说的,就是我们常见的函数,其中要注意一点是,在golang里,函数的局部变量是在函数被调用时才,变量才初始化,比如int类型的变量,然后初始化为0,当函数调用完毕之后,这些局部变量就会被释放。如上面例子,调用多少次,那么x就初始化,然后释放,下次调用的时候仍然是初始化,然后再释放。
下面我们来看看闭包的变量

闭包的变量

package main

import "fmt"

// 函数的返回值是一个匿名函数,返回一个函数类型
func test2() func() int {
	var x int
	// 对于下面的代码,匿名函数就形成了一个闭包了
	return func() int {
		x++
		return x * x
	}
}

func main() {
	// 返回值为一个匿名函数,返回一个函数类型,通过f来调用返回的匿名函数,f来调用闭包函数
	f := test2()
	// 它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包(这里指的是f)还在使用它,这些变量就还会存在
	fmt.Println(f()) // 1
	fmt.Println(f()) // 4
	fmt.Println(f()) // 9
	fmt.Println(f()) // 16
	fmt.Println(f()) // 25

}

上面代码,在闭包第一次被调用完之后,一下代码就形成了一个独立的空间

	return func() int {
		x++
		return x * x
	}

x 的生命周期还在,也没有被释放。那x还是1,当第一次调用之后,x=1,第二次调用的时候x++ 就是2,然后第三次x++ = 3…所以返回结果就是他们的平方,1,4,9,16,25.

函数test2返回另一个类型为func() int 的函数。对test2的一次调用会生成一个局部变量x并返回一个匿名函数。每次调用时匿名函数时,该函数都会先使x的值加1,再返回x的平方。第二次调用test2 时,会生成第二个x变量,并返回一个新的匿名函数。新匿名函数操作的是第二个x变量。
通过这个例子,我们看到变量的生命周期不由它的作用域决定:test2返回后,变量x仍然隐式的存在于f中。
所以对于开始的那句话:它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只要闭包函数(这里指main方法的f)还在使用它,这些变量就还会存在。

defer

官方定义:关键字 defer用于延迟一个函数或者方法(或者当前所创建的匿名函数)的执行。注意,defer语句只能出现在函数或方法的内部。

说人话:defer主要用于函数在调用结束前做一些清理工作,比如我们读取文件,文件打开了,然后我要关闭文件,什么时候关闭呢,那就是函数结束前去关闭。

延迟:在函数执行完毕之前调用

defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下

package main

import "fmt"

func main() {

	// defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下
	defer fmt.Println("我自横刀向天笑")
	fmt.Println("去留肝胆两昆仑")
}

控制台:

PS D:\vscode\code\demo1> go run defer1.go
去留肝胆两昆仑
我自横刀向天笑

多个defer执行顺序

如果一个函数中有多个defer语句,它们会以LFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
其实可以理解为和栈差不多,先进后出,就是按照代码顺序,先defer的后被执行。

下面我们来看一段代码

package main

import "fmt"

func test(x int) {
	result := 10 / x
	fmt.Println("result = %d", result)
}

func main() {

	// defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下
	fmt.Println("不尽长江滚滚来")
	fmt.Println("无边落木萧萧下")
	test(0)
	fmt.Println("驻青沙白鸟飞回")
	fmt.Println("风急天高猿啸哀")
}

下面我们通过代码来解释一下这句话:
如果一个函数中有多个defer语句,它们会以LFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
demo1:普通代码,没有defer

PS D:\vscode\code\demo1> go run defer1.go
不尽长江滚滚来
无边落木萧萧下
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.test(0xf366c0)
        D:/vscode/code/demo1/defer1.go:6 +0xad
main.main()
        D:/vscode/code/demo1/defer1.go:15 +0x9b
exit status 2

demo分析:以上代码没有defer我们可以看到,以上代码遇到panic的时候就停止运行了。

demo2:其他代码加defer,但是panic的那行代码不加defer

package main

import "fmt"

func test(x int) {
	result := 10 / x
	fmt.Println("result = %d", result)
}

func main() {

	// defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下
	defer fmt.Println("不尽长江滚滚来")
	defer fmt.Println("无边落木萧萧下")
	test(0)
	defer fmt.Println("驻青沙白鸟飞回")
	defer fmt.Println("风急天高猿啸哀")
}

控制台

PS D:\vscode\code\demo1> go run defer1.go
无边落木萧萧下
不尽长江滚滚来
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.test(0x101018c6cbd0108)
        D:/vscode/code/demo1/defer1.go:6 +0xad
main.main()
        D:/vscode/code/demo1/defer1.go:15 +0xc5
exit status 2
PS D:\vscode\code\demo1>

demo2分析:代码里除了panic的那行代码没有defer,其他的都加了defer。我们可以看到两个现象。现象1:前两句诗确实是按照 先进后出,即多个defer修饰的代码先定义后打印。
现象2,panic之后的代码并没有被打印,也就是在panic之后的代码就没运行。
注意:以上代码其实最先调用的是test(0)这行代码,因为被defer修饰代码在函数运行结束之前才会被调用。其次运行到代码test(0)的时候代码崩了,然后函数就要结束了,所以此时要运行被defer修饰的代码,因为test(0)没有被defer修饰,所以test(0)之后的代码就不会被运行了。于是打印结果是只打印了,前两个derfer。所以我们在写代码的时候,尽量将defer写在函数最前面。

demo3:所有代码都加defer

package main

import "fmt"

func test(x int) {
	result := 10 / x
	fmt.Println("result = %d", result)
}

func main() {

	// defer是一个延迟的作用,在函数执行完毕之前调用,所以defer修饰的代码会被在最后执行,如下
	defer fmt.Println("不尽长江滚滚来")
	defer fmt.Println("无边落木萧萧下")
	defer test(0)
	defer fmt.Println("驻青沙白鸟飞回")
	defer fmt.Println("风急天高猿啸哀")
}

控制台

PS D:\vscode\code\demo1> go run defer1.go
风急天高猿啸哀
驻青沙白鸟飞回
无边落木萧萧下
不尽长江滚滚来
panic: runtime error: integer divide by zero

goroutine 1 [running]:
main.test(0xab6220)
        D:/vscode/code/demo1/defer1.go:6 +0xad
main.main()
        D:/vscode/code/demo1/defer1.go:18 +0x19a
exit status 2
PS D:\vscode\code\demo1>

dem1:你知道运行结果吗?
demo3分析:
现象1:多个defer修饰的代码全部是,【先进后出】即:先定义后运行的顺序。
现象2:哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

所以以上3个demo就解释了开头的那句话:
如果一个函数中有多个defer语句,它们会以LFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。

defer 关键字后面表达式的求值时机(defer和匿名函数联合使用)

defer和匿名函数联合使用是开发中非常容易遇到的坑,而且不易排查,大家一定注意!!!
这里大家一定要牢记一句话:
defer 关键字后面的表达式,是在将 deferred 函数注册到
deferred 函数栈的时候进行求值的。

defer 关键字后面的表达式,是在将 deferred 函数注册到
deferred 函数栈的时候进行求值的。

defer 关键字后面的表达式,是在将 deferred 函数注册到
deferred 函数栈的时候进行求值的。

有了上面的无上心法,下面我们来实战一下:

package main

import "fmt"

func test(x int) {
	result := 10 / x
	fmt.Println("result = %d", result)
}

func main() {

	a := 10
	b := 20

	defer func() {
		fmt.Printf("匿名函数 a=%d, b=%d\n", a, b)
	}() // 调用函数

	a = 111
	b = 222
	fmt.Printf("外部 a = %d, b =%d\n", a, b)
}

控制台:

PS D:\vscode\code\demo1> go run defer2.go
外部 a = 111, b =222
匿名函数 a=111, b=222
PS D:\vscode\code\demo1> 

demo1解析:a,b分别被初始化为10,20,然后defer代码被压入 deferred 函数栈,此时被压入deferred函数栈的函数是

func()

然后a,b分别被赋值111,222然后输出fmt语句。
因为 defer是最后执行的,所以先打印外部函数,再打印匿名函数。于是当代码中fmt语句先打印出来fmt语句。于是执行defer语句,此时匿名函数会以闭包的方式访问外围函数的变量,
此时两个变量早已经被赋予了新的值111,222,于是defer打印的就是新的值了。

有人说demo1很简单,那我们来看看demo2
demo2:

package main

import "fmt"

func test(x int) {
	result := 10 / x
	fmt.Println("result = %d", result)
}

func main() {

	a := 10
	b := 20

	defer func(a, b int) {
		fmt.Printf("匿名函数 a=%d, b=%d\n", a, b)
	}(a, b) // 调用函数

	a = 111
	b = 222
	fmt.Printf("外部 a = %d, b =%d\n", a, b)
}

控制台

PS D:\vscode\code\demo1> go run defer2.go
外部 a = 111, b =222
匿名函数 a=10, b=20
PS D:\vscode\code\demo1>

WTF?

demo2解析:a,b分别被初始化为10,20,然后defer代码被压入 deferred 函数栈,此时被压入deferred函数栈的函数(defer 关键字后面的表达式,是将 deferred 函数注册到deferred 函数栈的时候进行求值的,所以也将此时a,b的值10,20一并压入到函数栈里了)

func(10, 20)

然后a,b分别被赋值111,222然后输出fmt语句。
因为 defer是最后执行的,所以先打印外部函数,再打印匿名函数。于是当代码中fmt语句先打印出来fmt语句。于是执行defer语句,因为 defer 关键字后面的表达式,是将 deferred 函数注册到deferred 函数栈的时候进行求值的
此时执行的defer里的a,b依旧是defer函数被注册到函数栈时的a,b的值10,20。

defer func(a, b int) {
		fmt.Printf("匿名函数 a=%d, b=%d\n", a, b)
	}(10, 20) // 调用函数
defer func(a, b int) {
		fmt.Printf("匿名函数 a=%d, b=%d\n", a, b)
	}(a, b) // 调用函数

demo3

package main

import "fmt"

func f1() (result int) { 
	defer func() {
		result++
	}()
	return 1
}

func f2() (r int) { 
	t := 1
	defer func() {
		t++
	}()
	return t
}

func f3() (r int) { 
	defer func(t int) {
		t++
	}(r)
	return 1
}

func main() {
	fmt.Println(f1())
	fmt.Println(f2())
	fmt.Println(f3())
}

demo3
控制台

PS D:\vscode\code\demo1> go run defer3.go
2
1
1

demo3解析:
f1函数


f1被压入deferred函数栈的函数为

func()

注意此时result为0,但是由于defer没有传参,于是result的值并没有被注册到函数栈。然后代码执行返回1,函数的返回变量为result,
也就是将1赋值给了result,于是测试result =1,接下来执行defer函数,
因为 **defer 关键字后面的表达式,是将 deferred 函数注册到deferred 函数栈的时候进行求值的** 
但是由于defer没有传参,于是result的值并没有被注册到函数栈,此时匿名函数会以闭包的方式访问外围函数的变量,
因为此时result为1 ,那么result++之后就是2了,然后代码执行完毕返回结果,于是f1函数返回值为2.

func main() {

	res := f1()
	fmt.Println("res=", res)
}

func f1() (result int) {
	defer func() {
		result++
		fmt.Println("result=", result)
	}()
	return 1
}

控制台
result= 2
res= 2

f2函数解析

f2被压入deferred函数栈的函数为

func()

注意此时t为1,但是由于defer没有传参,于是t和r的值都没有被注册到函数栈。然后代码执行返回t,函数的返回变量为r,
也就是将t赋值给了r,于是测试r =1,接下来执行defer函数,
因为 **defer 关键字后面的表达式,是将 deferred 函数注册到deferred 函数栈的时候进行求值的** 
但是由于defer没有传参,于是result的值并没有被注册到函数栈,此时匿名函数会以闭包的方式访问外围函数的变量,因为此时t为1 ,
那么t++之后就是2了,r仍然为1,然后代码执行完毕返回结果,返回的是r,而不是t,所以函数f2函数返回1
func main() {

	res := f2()
	fmt.Println("res=", res)
}

func f2() (r int) {
	t := 1
	defer func() {
		t++
		fmt.Println("t=", t)
	}()
	return t
}

控制台
t= 2
res= 1

f3函数解析

f3被压入deferred函数栈的函数为

func(0)

注意此时r为0,r被当成参数传给了defer的匿名函数,参数名为t,即r=0,t=r,此时t=0,
于是r的值被注册到函数栈
(因为defer 关键字后面的表达式,是将 deferred 函数注册到deferred 函数栈的时候进行求值的)。
然后代码执行返回1,函数的返回变量为r,也就是将1赋值给了r,于是此时r=1,接下来执行defer函数,
defer函数里的t是defer函数被注册的时候的r的值,为0,于是t++ ,t = 1 ,
并且该匿名函数的传值非引用传递,所以t的改变于r没有diao毛关系,于是代码执行完毕返回结果,
于是f3函数返回值为1.
// 如果defer这里传递的是r的引用,那结果就是2了

func main() {

	res := f3()
	fmt.Println("res=", res)
}

func f3() (r int) {
	defer func(t int) {
		t++
		fmt.Println("t=", t)
	}(r)
	return 1
}
控制台
t= 1
res= 1

如果f3函数传递的是引用,则defer里计算的就是r的值了,那么函数返回的就是2了。
f3被压入deferred函数栈的函数为

func(r的地址) // 此时为0

注意此时r为0,r被当成参数传给了defer的匿名函数,参数名为t,即r=0,t=r,此时t=0,
于是r的值被注册到函数栈
(因为 defer 关键字后面的表达式,是将 deferred 函数注册到deferred 函数栈的时候进行求值的)。
然后代码执行返回1,函数的返回变量为r,也就是将1赋值给了r,于是此时r=1,接下来执行defer函数,
defer函数里的t是defer函数被注册的时候的r的值,
但是由于是引用传递,t于r指向的是同一个地址值,于是t= 1defer函数取t变量的值的时候,t也为1,
于是t++ ,t =2 ,
于是代码执行完毕返回结果,于是f3函数返回值为2,t的值也为2.
// 注意:*类型,如*int代表该变量为指针类型,
用来存储变量的地址。*指针类型的*代表对指针取值,
取出指针指向的地址的内存。&代表取一个变量的指针地址,此时f3函数返回值为2,t的值也为2.
func f3() (r int) {
	defer func(t *int) {
		*t++
	}(&r)
	return 1
}

注:*、&,指针和内存

defer + recover 让程序从panic恢复

由于程序遇到panic后,仍然会执行defer里的代码,据此特性,我们可以利用defer拦截panic,并按需对panic进行处理,且defer与recover配合是Go语言中唯一的从panic中恢复的手段

需求:要让main函数中的fmt.Println(“main exit normally”)代码被执行

demo1:正确捕获panic并让程序从panic中恢复

看如下代码,猜运行结果

package main

import "fmt"

func main() {
	foo()
	fmt.Println("main exit normally")
}

func foo() {
	defer func() {
		//如果没有这个recover()
		if e := recover(); e != nil {
			fmt.Println("recovered from a panic")
		}
	}()
	bar()
	fmt.Println("xxxx")
}

func bar() {
	fmt.Println("raise a panic")
	panic(-1)
	fmt.Println("zzzzz")
}

控制台

go run test3.go
raise a panic
recovered from a panic
main exit normally

啰嗦版(解析均在啰嗦版)

package main

import "fmt"

func main() {
	foo() //由于foo函数里对panic做了处理,于是panic没有被抛出到main函数,于是下面的fmt.Println能被执行
	fmt.Println("main exit normally")
}

func foo() {
	defer func() {
		//如果没有这个recover(),且该recover不在defer内,则panic就会抛出到调用foo函数的外部函数里,这里就抛出到main函数了,如果没有这个代码则main函数就停止运行了
		if e := recover(); e != nil {
			fmt.Println("recovered from a panic")
		}
	}()
	bar() //因为bar函数里panic了,于是panic向上抛,函数遇到panic就停止,然后执行defer里的函数,然后下面这行代码执行不了
	fmt.Println("xxxx")
}

func bar() {
	fmt.Println("raise a panic")
	panic(-1) // 遇到panic函数就不再执行,于是下面代码执行不了
	fmt.Println("zzzzz")
}

demo2:demo1为何能让程序从panic恢复进行分析

将demo1中的foo函数改为如下场景

func foo() {
	defer recover();
	bar()
	fmt.Println("xxxx")
}

控制台

o run test3.go
raise a panic
panic: -1

goroutine 1 [running]:
main.bar()
        /Users/go_demo/test3.go:23 +0x88
main.foo()
        /Users/go_demo/test3.go:17 +0x4c
main.main()
        /Users/go_demo/test3.go:6 +0x20
exit status 2

或将foo改为如下场景

func foo() {
	defer func() {
		fmt.Println("recovered from a panic")
	}()

	bar()
	fmt.Println("xxxx")
}

控制台

go run test3.go
raise a panic
recovered from a panic
panic: -1

goroutine 1 [running]:
main.bar()
        /Users/didi/go_demo/test3.go:21 +0x88
main.foo()
        /Users/didi/go_demo/test3.go:15 +0x3c
main.main()
        /Users/didi/go_demo/test3.go:6 +0x20
exit status 2

再foo函数改为如下场景

	defer func() {
		recover()
	}()
	
	bar()
	fmt.Println("xxxx")

控制台

go run test3.go
raise a panic
main exit normally

由此可见defer只能拦截住panic,却不能让程序从panic中恢复过来。
所谓defer只能拦截住panic,也只是在发生panic时会停止运行panic之后的代码,并执行函数里的derfer代码,但是defer并不能让程序从panic里恢复过来,真正使程序从panic里恢复过来的是recover函数,我们来看下recover函数的定义

// The recover built-in function allows a program to manage behavior of a
// panicking goroutine. Executing a call to recover inside a deferred
// function (but not any function called by it) stops the panicking sequence
// by restoring normal execution and retrieves the error value passed to the
// call of panic. If recover is called outside the deferred function it will
// not stop a panicking sequence. In this case, or when the goroutine is not
// panicking, or if the argument supplied to panic was nil, recover returns
// nil. Thus the return value from recover reports whether the goroutine is
// panicking. 
func recover() interface{}

大意就是说,如果在defer 函数 内部执行recover,则会让程序从panic中恢复过来,如果是在函数之外调用recover则不能将代码从panic中恢复过来。
所以这也就解释了,为啥foo函数里这样写不行:defer recover();,因为这里的recover函数,不在defer 函数 的内部,这里重要的事情说3遍,
defer 函数 内部执行recover,
defer 函数 内部执行recover,
defer 函数 内部执行recover,
所以得是 defer func(){}()的形式,而recover函数必须在该函数的内部,即defer func(){recover()}()
如我们将foo代码改为如下形式

func foo() {
	defer func() {
		recover()
	}()
	
	bar()
	fmt.Println("xxxx")
}

控制台:完美解决问题。

go run test3.go
raise a panic
main exit normally
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值