什么是闭包
前言
在进入今天的课程前,我想和大家一起复习一下之前的一些概念,首先我们在有关函数的文章中介绍了函数是Go语言的一等公民,不同于其他语言,函数在Go语言里面扮演的角色很多,下面我们来看一下:
- 作为函数的参数来传递(也叫回调函数)
func printOneParam(p int) { fmt.Println(p) } func printOnePlus(p int) { fmt.Println(p + 1) } func funcParamOne(f func(a int)) { num := 100 f(num) } func TestFuncParamOne(t *testing.T) { funcParamOne(printOneParam) funcParamOne(printOnePlus) }
- 作为变量
package main import "fmt" func Print() func(){ return func(){ fmt.Println("Hello World") } } func main(){ my_func:=Print() my_func() }
- 单纯的返回值
func sum(a,b int){ return a+b }
像以上无论是func(a int)
,my_func
还是a+b
这样参数在Go语言中被称为function value
,我们知道函数指令的生成在编译时期生成,我们需要找到函数入口的地址来执行它,但是其实function value
并不直接指向函数指令入口,它作为指针指向的是一个在runtime库中的结构体,
runtime.funcval
结构体:
type funcval struct{ fn uniptr //指向函数入口的指针 extra *uint8 //指向存储数据的指针 }
开始有一个小问题:在这里我们可以看到,在调用函数过程中,其实本质上是使用了有关二级指针来访问函数,这样看起来十分的繁琐,那这样设计的目的是什么呢?这就要提到我们今天的主角——闭包了
什么是闭包
关于什么是闭包,博主也查阅了很多资料于文章,首先我们来看一下百度百科给出的答案:
闭包包含自由(未绑定到特定对象)变量,这些变量不是在这个代码块内或者任何全局上下文中定义的,而是在定义代码块的环境中定义(局部变量)。“闭包” 一词来源于以下两者的结合:要执行的代码块(由于自由变量被包含在代码块中,这些自由变量以及它们引用的对象没有被释放)和为自由变量提供绑定的计算环境(作用域) • 1
看这个大家有可能会感到很懵,我们来截取一下关键词,首先闭包里面要有在函数外部定义但是在函数内部被引用的自由变量,还有就是即使脱离了捕捉自由变量的上下文,闭包也可以正常使用这些自由变量
上面的乍一看可能还是会让人感觉非常晕,那就让我们来看下面这个例子:
package main import "fmt" func create_func () func() int{ c:=2 return func() int{ return c } } func main(){ f1:=create_func() f2:=create_func() fmt.Println(f1()) fmt.Println(f2()) }
这个函数的输出结果为:
2 2
这里的c
也就是我们所说的自由变量了。
或许我说的大家还不是很明白,我来给大家分析一下这个代码里面的堆栈调用大家就明白了:
首先是函数栈,示例代码里面一共有两个函数栈,一个是main函数栈
,一个是create函数栈
首先我们来看main函数栈一共两个局部变量分别是f1与f2,除此之外就是main函数的返回值
但是create函数里面存在闭包,所以除了局部变量和返回值以外,它会在堆中去创建一个funcval
结构体,而这里面的funcval
的值有两个:捕获的自由变量以及匿名函数的入口。每一次创建的结构体也会被赋值给f1与f2
扩展:当我们利用funcval去调用一个函数的时候其实是通过寄存器来读取结构体的地址继而调用函数,这里不做深入,大家有兴趣的话可以自行做了解。
示例分析
最后我们来看下面的例子:
func main() { sum := Sum(1, 2) fmt.Println(sum(3, 4)) fmt.Println(sum(5, 6)) } func Sum(a, b int) func(int, int) int { return func(int, int) int { return a + b } }
我在没有学习今天讲的这些东西之前我以为它的输出应该是这样的:
3 7 11
但是实际上它的输出结果是
3 3 3
这是为什么呢,主要是因为这里的a,b都是自由变量,它们在Sum函数中被捕获的时候就被抓到堆里面去了,我们如果学过内存分配的话应该知道,堆里面的变量不会像栈变量随函数结束上面周期而被销毁,所以这里的a=1
b=2
除非我们手动重新赋值,比如:
a=2 b=3
否则不会因为传入匿名函数的参数不同而改变,故输出值始终是3.
总结
闭包函数的处理方式十分多样,这里我也只是揭露了它的冰山一角,大家有兴趣的话可以继续做深入学习,但其实无论处理方式如何变化,其本质也只不过是为了保证捕获变量在外层函数与闭包函数的一致性,这是它的核心存在
尾语
本来有关闭包的部分我是想放在上一篇文章中的,但是有关捕获变量等问题一时间不是很明白,就决定将它单独的拿出来作为Go语言进阶篇的前传,从明天开始我就将开始有关于接口以及面向对象的有关内容了,大家下篇文章见,最后大家如果感到有收获的话,还请三连一下吧!!!!