闭包以引用的方式捕捉外部变量
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修饰的代码会被在最后执行,如下
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>```
demo3分析:
现象1:多个defer修饰的代码全部是,【先进后出】即:先定义后运行的顺序。
现象2:哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
所以以上3个demo就解释了开头的那句话:
如果一个函数中有多个defer语句,它们会以LFO(后进先出)的顺序执行。哪怕函数或某个延迟调用发生错误,这些调用依旧会被执行。
# defer和匿名函数联合使用
无奖精彩,爱猜不猜
dem1:你知道运行结果吗?
```go
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分析,首先defer是最后执行的,所以先打印外部函数,再打印匿名函数。又因为两个变量已经被赋予了新的值,于是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分析:首先先打印外部的,并且外部打印的为111,222这个没啥争议。
但是匿名函数内呢?其实代码相当于是先把参数10,20传进去了,但是并没有马上运行,因为它被defer修饰了,所以它最后运行,但是当运行的时候,它拿到的还是传递过去的10,20,所以打印结果是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解析:
package main
import "fmt"
// 首先函数的返回值为result,在return 1 的时候,其实是将1 赋值给了result这个变量,然后defer是最后运行的,
// result++ ,所以返回值为2
func f1() (result int) {
defer func() {
result++
}()
return 1
}
// 首先函数的返回值为r,在return的时候,其实是将t的值赋值给了r,于是r = 1,然后defer是最后运行的
// 但是返回值为r,defer里面对t进行++,t = 2,但是返回值是r,所以函数返回值为1
func f2() (r int) {
t := 1
defer func() {
t++
}()
return t
}
// 首先函数返回值为r, return 1的时候其实是将 1赋值给了r,然后函数传参,将r 传递了进去,此时t = 1,然后t++ = 2
// 但是函数的返回值为r,与t无关,所以返回1,可以把defer函数里的r参数看成一个固定值1,就行了,不要被干扰
// 还可以这样理解,go语言中所有的函数传递都是传值的,那么所有函数都是有一个参数副本的,在函数里面计算是计算的副本,
// 不会修改原先的值,也就是这里虽然传递的是r,但是它算的是r的副本,并不是r原先的值,如果这里传递的是r的引用,那结果就是2了
func f3() (r int) {
defer func(t int) {
t++
}(r)
return 1
}
// 如果传递的是引用,则defer里计算的就是r的值了,那么函数返回的就是r了。
// 注意:*类型,如*int代表该变量为指针类型,用来存储变量的地址。*指针类型的*代表对指针取值,取出指针指向的地址的内存。&代表取一个变量的指针地址
func f3() (r int) {
defer func(t *int) {
*t++
}(&r)
return 1
}
func main() {
fmt.Println(f1())
fmt.Println(f2())
fmt.Println(f3())
}