1.什么是闭包
官方的讲,闭包是指可以包含自由变量(未绑定到特定对象)变量的代码块;这些变量不是在这个代码块内活在任何全局上下文中定义的,而是在定义代码块的环境中定义的(局部变量),当在这个代码块所在环境的外部调用该代码块时,代码块和它所引用的自由变量构成闭包。如下图所示。
2.闭包的作用
从图1很容易看出,自由变量是局部变量,外部无法访问,但和它同属一个局部环境的代码块却可以访问,因此,我们可以通过代码块间接地访问内部的自由变量。如果代码块理解起来有点儿抽象,我们下面看一个具体的例子:
package main
import "fmt"
func A() func(){
// n是自由变量
var n int = 2019
// 匿名函数相当于图1中的“代码块”
return func() {
n += 1
fmt.Println(n)
}
}
// main()在外部环境中
func main() {
myFunc := A()
// 外部环境调用代码块
myFunc()
myFunc()
}
在上面的代码中,函数A内部有一个局部变量n, 为了能在外部环境中访问n,我们让函数A返回了一个匿名函数,这个匿名函数有对n的访问权限,我们在main函数调用函数A返回的匿名函数,可以修改并打印出n的值,代码编译运行的结果如下:
2020
2021
Process finished with exit code 0
可以看出,这个A内部的匿名函数就是函数A暴露给外部访问其内部变量n的一个“接口”,通过这个接口,调用者可以实现在全局环境中访问局部变量。
3.闭包的好处
- 我们再来观察上面代码的运行结果,发现在main函数中两次调用A的匿名函数,结果居然不同。
- 一般情况下,在函数func执行完后,函数之前申请的栈空间会被释放,函数中的局部变量也会被销毁,下次再出现函数调用时,重新申请栈空间,并且重新初始化函数内部的局部变量。
- 但是在闭包调用的情况下,情况会变得不同,由于在main函数调用了A返回的匿名函数,相当于myFunc = func() {n += 1; fmt.Println(n);},并且匿名函数内部引用着A里的局部变量n,所以导致在main函数一次函数调用结束后,n无法被销毁,因为它此时整被main函数中的myFunc引用着,它的生命周期和myFunc的生命周期相同,在main函数执行完毕时才会被释放。
- 因此,当使用闭包方式访问某个局部变量时,该局部变量会常驻内存,访问速度会特别快,并且我们始终操作的都是这个变量的本身,而不是其副本,修改时也特别方便。闭包调用的大致过程如下图所示(不是很准确,会意即可!)
-
为了更好地理解闭包,我们再看一个例子。
- 编写一个函数makeSuffix(suffix string)可以接收一个文件后缀名(比如.jpg),并返回一个匿名函数;
- 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(比如.jpg),则返回文件名.jpg, 如果已经有.jpg后缀,则返回原文件名。
- 代码如下所示:
package main
import (
"fmt"
"strings"
)
// 处理文件名
func makeSuffix(suffix string) func (string) string {
return func(name string) string {
// 匿名函数绑定的局部变量是外部函数形参suffix
if !strings.HasSuffix(name, suffix) {
// 如果文件名没有指定的后缀名,则给它加上指定的后缀名
name += suffix
}
return name
}
}
func main() {
// 1.指定文件后缀名为.jpg
f := makeSuffix(".jpg")
// 2.创建文件名
fileName1 := "flower"
fileName2 := "flower.jpg"
// 3.调用f
res1 := f(fileName1)
res2 := f(fileName2)
// 4.打印闭包调用处理后的结果
fmt.Println("res1=", res1)
fmt.Println("res2=", res2)
}
- 编译运行后的结果如下:
res1= flower.jpg
res2= flower.jpg
Process finished with exit code 0
- 从结果可以看出,原来不是以后缀.jpg结尾的文件名被加上了.jpg,以.jpg结尾的文件名没有变化。
- 在上述代码中,返回的匿名函数和makeSuffix(suffix string)的suffix变量组成了闭包关系,因为返回的匿名函数引用到了这个变量。
- 我们体会一下闭包的好处,如果使用传统的方法,也可以很容易实现这个功能,但是传统需要每次都传入后缀名,比如.jpg, 而闭包是因为初次调用时闭包绑定的变量已经常驻内存,所以传入一次就可以反复使用。
4.闭包的总结
(1) 通常是通过嵌套的匿名函数的形式实现的;
(2) 匿名函数内部引用了外部函数的参数或变量;
(3) 被引用的参数和变量的生命周期和外部调用者的生命周期相同。
5.防止闭包的误用
请看下面的代码:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(10);
for i:=0; i<10; i++ {
// 以匿名函数的形式开启goroutine
go func() {
fmt.Println(i)
wg.Done()
}()
}
wg.Wait()
}
- 在main函数中由于在匿名函数引用了外部变量i,因此匿名函数以闭包的形式访问i, 但同时for循环中使用了goroutine,由于goroutine之间是并发执行的,再参考图2闭包调用的流程图,就会出现多个goroutine访问同一个内存中的变量,会出现“脏读”现象,代码编译执行如下:
2
7
7
3
7
10
10
10
10
7
Process finished with exit code 0
- 为了解决这个问题,也很简单,我们只需让for循环时每个匿名函数绑定的不是外部变量i,而是i的副本,如何解决呢?之间用匿名函数传参的形式,由于go语言的函数传参都是值传递,这样就可以通过值传递来为每个goroutine的匿名函数复制出一个当前i的副本,所有的goroutine在同时执行时互不影响。代码如下:
package main
import (
"fmt"
"sync"
)
var wg sync.WaitGroup
func main() {
wg.Add(10);
for i:=0; i<10; i++ {
// 以匿名函数的形式开启goroutine
go func(num int) {
fmt.Println(num)
wg.Done()
}(i)
}
wg.Wait()
}
编译执行结果如下:
5
6
2
0
7
9
3
8
1
4
Process finished with exit code 0
可以发现,每个goroutine打印的结果都不一样,打印顺序随机是由于goroutine之间的并发执行造成的,我们通过匿名函数传参的形式就解决了这种由于不经意间使用了闭包(自己没有发现)带来的错误,在go语言的并发编程中,这种情况比较多见,我们应该格外注意。
闭包问题的讨论就到这里了,各位看官下期再见~~~
我是lioney,年轻的后端攻城狮一枚,爱钻研,爱技术,爱分享。
个人笔记,整理不易,感谢阅读、点赞和收藏。
文章有任何问题欢迎大家指出,也欢迎大家一起交流后端各种问题!
有疑问加站长微信联系(非本文作者)