闭包是主流编程语言中的一种通用技术,经常和函数式编程进行强强联合,本文主要是介绍 Go
语言中什么是闭包以及怎么理解闭包.html
若是读者对于 Go
语言的闭包还不是特别清楚的话,能够参考上一篇文章 go 学习笔记之仅仅须要一个示例就能讲清楚什么闭包.linux
或者也能够直接无视,由于接下来会回顾一下前情概要,如今你准备好了吗? Go
!git
不管是 Go
官网仍是网上其余讲解闭包的相关教程,总能看到斐波那契数列的身影,足以说明该示例的经典!github
斐波那契数列(
Fibonacci sequence
),又称黄金分割数列 .因数学家列昂纳多·斐波那契(Leonardoda Fibonacci
)以兔子繁殖为例子而引入,故又称为“兔子数列”,指的是这样一个数列:一、一、二、三、五、八、1三、2一、3四、……
在数学上,斐波那契数列以以下被以递推的方法定义:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
.在现代物理、准晶体结构、化学等领域,斐波纳契数列都有直接的应用,为此,美国数学会从1963年起出版了以《斐波纳契数列季刊》为名的一份数学杂志,用于专门刊载这方面的研究成果.编程
根据上述百度百科的有关描述,咱们知道斐波那契数列就是形如 1 1 2 3 5 8 13 21 34 55
的递增数列,从第三项开始起,当前项是前两项之和.数组
为了计算方便,定义两个变量 a,b
表示前两项,初始值分别设置成 0,1
,示例:缓存
// 0 1 1 2 3 5 8 13 21 34 55
// a b
// a b
a, b := 0, 1
复制代码
初始化后下一轮移动,a, b = b, a+b
结果是 a , b = 1 , 1
,恰好可以表示斐波那契数列的开头.闭包
「雪之梦技术驿站」试想一下: 若是
a,b
变量的初始值是1,1
,不更改逻辑的状况下,最终生成的斐波那契数列是什么样子?app
func fibonacciByNormal() {
a, b := 0, 1
a, b = b, a+b
fmt.Print(a, " ")
fmt.Println()
}
复制代码
可是上述示例只能生成斐波那契数列中的第一个数字,假如咱们须要前十个数列,又该如何?编程语言
func fibonacciByNormal() {
a, b := 0, 1
for i := 0; i < 10; i++ {
a, b = b, a+b
fmt.Print(a, " ")
}
fmt.Println()
}
复制代码
经过指定循环次数再稍加修改上述单数列代码,如今就能够生成前十位数列:
// 1 1 2 3 5 8 13 21 34 55
func TestFibonacciByNormal(t *testing.T) {
fibonacciByNormal()
}
复制代码
这种作法是接触闭包概念前咱们一直在采用的解决方案,相信稍微有必定编程经验的开发者都能实现,可是闭包却提供了另外一种思路!
// 1 1 2 3 5 8 13 21 34 55
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
复制代码
不管是普通函数仍是闭包函数,实现斐波那契数列生成器函数的逻辑不变,只是实现不一样,闭包返回的是内部函数,留给使用者继续调用而普通函数是直接生成斐波那契数列.
// 1 1 2 3 5 8 13 21 34 55
func TestFibonacci(t *testing.T) {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Print(f(), " ")
}
fmt.Println()
}
复制代码
对于这种函数内部嵌套另外一个函数而且内部函数引用了外部变量的这种实现方式,称之为"闭包"!
「雪之梦技术驿站」: 闭包是函数+引用环境组成的有机总体,二者缺一不可,详细请参考go 学习笔记之仅仅须要一个示例就能讲清楚什么闭包.
「雪之梦技术驿站」: 自带运行环境的闭包正如电影中出场自带背景音乐的发哥同样,音乐响起,发哥登场,闭包出现,环境自带!
闭包自带独立的运行环境,每一次运行闭包的环境都是相互独立的,正如面向对象中类和对象实例化的关系那样,闭包是类,闭包的引用是实例化对象.
func autoIncrease() func() int {
i := 0
return func() int {
i = i + 1
return i
}
}
复制代码
上述示例是闭包实现的计算器自增,每一次引用 autoIncrease
函数得到的闭包环境都是彼此独立的,直接上单元测试用例.
func TestAutoIncrease(t *testing.T) {
a := autoIncrease()
// 1 2 3
t.Log(a(), a(), a())
b := autoIncrease()
// 1 2 3
t.Log(b(), b(), b())
}
复制代码
函数引用 a
和 b
的环境是独立的,至关于另外一个如出一辙计数器从新开始计数,并不会影响原来的计数器的运行结果.
「雪之梦技术驿站」: 闭包不只仅是函数,更加剧要的是环境.从运行效果上看,每一次引用闭包函数从新初始化运行环境这种机制,很是相似于面向对象中类和实例化对象的关系!
普通函数内部定义的变量寿命有限,函数运行结束后也就被系统销毁了,结束了本身短暂而又光荣的一辈子.
可是,闭包所引用的变量却不同,只要一直处于使用中状态,那么变量就会"长生不老",并不会由于出身于函数内就和普通变量拥有同样的短暂人生.
func fightWithHorse() func() int {
horseShowTime := 0
return func() int {
horseShowTime++
fmt.Printf("(%d)祖国须要我,我就提枪上马当即战斗!\n",horseShowTime)
return horseShowTime
}
}
func TestFightWithHorse(t *testing.T) {
f := fightWithHorse()
// 1 2 3
t.Log(f(), f(), f())
}
复制代码
「雪之梦技术驿站」: 若是使用者一直在使用闭包函数,那么闭包内部引用的自由变量就不会被销毁,一直处于活跃状态,从而得到永生的超能力!
凡事有利必有弊,闭包不死则引用变量不灭,若是不理解变量长生不老的特性,编写闭包函数时可能一不当心就掉进做用域陷阱了,千万要当心!
下面以绑定循环变量为例讲解闭包做用域的陷阱,示例以下:
func countByClosureButWrong() []func() int {
var arr []func() int for i := 1; i <= 3; i++ {
arr = append(arr, func() int {
return i
})
}
return arr
}
复制代码
countByClosureButWrong
闭包函数引用的自由变量不只有 arr
数组还有循环变量 i
,函数的总体逻辑是: 闭包函数内部维护一个函数数组,保存的函数主要返回了循环变量.
func TestCountByClosure(t *testing.T) {
// 4 4 4
for _, c := range countByClosureButWrong() {
t.Log(c())
}
}
复制代码
当咱们运行 countByClosureButWrong
函数得到闭包返回的函数数组 arr
,而后经过 range
关键字进行遍历数组,获得正在遍历的函数项 c
.
当咱们运行 c()
时,指望输出的 1,2,3
循环变量的值,可是实际结果倒是 4,4,4
.
缘由仍然是变量长生不老的特性:遍历循环时绑定的变量值确定是 1,2,3
,可是循环变量 i
却没有像普通函数那样消亡而是一直长生不老,因此变量的引用发生变化了!
长生不老的循环变量的值恰好是当初循环的终止条件 i=4
,只要运行闭包函数,不管是数组中的哪一项函数引用的都是相同的变量 i
,因此所有都是 4,4,4
.
既然是变量引用出现问题,那么解决起来就很简单了,不用变量引用就行了嘛!
最简单的作法就是使用短暂的临时变量 n
暂存起来正在遍历的值,闭包内引用的变量再也不是 i
而是临时变量 n
.
func countByClosureButWrong() []func() int {
var arr []func() int for i := 1; i <= 3; i++ {
n := i
fmt.Printf("for i=%d n=%d \n", i,n)
arr = append(arr, func() int {
fmt.Printf("append i=%d n=%d\n", i, n)
return n
})
}
return arr
}
复制代码
上述解决办法很简单就是采用临时变量绑定循环变量的值,而不是原来的长生不老的变量引用,可是这种作法不够优雅,还能够继续简化进行版本升级.
既然是采用变量赋值的作法,是否是和参数传递中的值传递很相像?那咱们就能够用值传递的方式从新复制一份变量的值传递给闭包函数.
func countByClosureWithOk() []func() int {
var arr []func() int for i := 1; i <= 3; i++ {
fmt.Printf("for i=%d \n", i)
func(n int) {
arr = append(arr, func() int {
fmt.Printf("append n=%d \n", n)
return n
})
}(i)
}
return arr
}
复制代码
「雪之梦技术驿站」: 采用匿名函数自执行的方式传递参数
i
,函数内部使用变量n
绑定了外部的循环变量,看起来更加优雅,有逼格!
采用匿名函数进行值传递进行改造后,咱们再次运行测试用例验证一下改造结果:
func TestCountByClosureWithOk(t *testing.T) {
// 1 2 3
for _, c := range countByClosureWithOk() {
t.Log(c())
}
}
复制代码
终于解决了正确绑定循环变量的问题,下次再出现实际结果和预期不符,不必定是 bug
有多是理解不深,没有正确使用闭包!
「雪之梦技术驿站」: 每次调用闭包函数所处的环境都是相互独立的,这种特性相似于面向对象中类和实例化对象的关系.
「雪之梦技术驿站」: 长生不老的特性使得闭包引用变量能够常驻内存,用于缓存一些复杂逻辑代码很是合适,避免了原来的全局变量的滥用.
「雪之梦技术驿站」: 普通函数转变成闭包函数不只实现起来有必定难度,并且理解起来也不容易,不只要求多测试几遍还要理解闭包的特性.
「雪之梦技术驿站」: 过多使用闭包势必形成引用变量一直常驻内存,若是出现循环引用或者垃圾回收不及时有可能形成内存泄漏问题.
闭包是一种通用技术,Go
语言支持闭包,主要体如今 Go
支持函数内部嵌套匿名函数,但 Go
不支持普通函数嵌套.
简单的理解,闭包是函数和环境的有机结合总体,独立和运行环境和长生不老的引用变量是闭包的两大重要特征.
不管是模拟面向对象特性,实现缓存仍是封装对象等等应用都是这两特性的应用.
最后,让咱们再回忆一下贯穿始终的斐波那契数列来结束这次闭包之旅!
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
复制代码
本文涉及示例代码: github.com/snowdreams1…
若是你以为本文对你有所帮助,欢迎点赞留言告诉我,你的鼓励是我继续创做的动力,不妨顺便关注下我的公众号「雪之梦技术驿站」,按期更新优质文章哟!