Go语言闭包研究
闭包是Go语言中一个非常抽象的概念,也是笔者在Go语言的学习过程中遇到的第一个难点,希望本文可以较为细致的将闭包的概念及特性介绍清楚。
从函数变量说起
在Go语言中,函数也是一个变量,有类型、有值、有地址、可以被赋值、引用等,函数的零值为nil,但函数之间不能进行比较运算。
什么是闭包
由于函数也是变量,自然的可以想到函数也是有地址的(对汇编和操作系统有一定了解后就会知道,每一次函数的调用都会在内存中为该函数分配一片区域,叫做栈帧),而闭包,就可以理解为将函数的地址赋值给了一个变量,又由于Go的变量会进行自动反向引用,所以可以将该变量也当作一个函数使用(且由于栈帧的概念,该变量中还保存着函数被赋值时的内部各个变量的状态)。下面给出例子对其进行展示:
package main
import "fmt"
func inc() func() int { //返回值为函数闭包
var x int
return func() int {
x++
return x
}
}
func main(){
i := incr() //i被赋值为函数闭包,i内保存了x的状态
fmt.printLn(i()) // 1
fmt.printLn(i()) // 2
fmt.printLn(i()) // 3
fmt.printLn(incr()()) //1
fmt.printLn(incr()()) //1
fmt.printLn(incr()()) //1
return
}
复制代码
最开始的三个输出中,由于i
内保存了x
的状态,故每次对i
的调用都会通过i
内保存的指向x
的指针修改x
的值,且状态继续保存在i
中,这种状态叫做x
的逃逸,它的生命周期没有随着作用域的结束而结束。
之后的三个输出时,由于每一次都调用一次incr()
返回的一个闭包,故三次的x
属于不同的栈帧,状态各自独立。
闭包引用
理解了闭包的定义之后,对于闭包的引用也就自然而然了。闭包的引用其实就和其他变量的引用一样,只不过引用闭包之后得到的是一个保存了闭包声明时的状态的函数。需要注意的是,闭包对于外层词法域变量是引用的,也即,在闭包外部的变量修改可能会影响闭包内部的值,通过以下例子进行说明:
x := 1
f := func() {
println(x)
}() //等价于定义之后调用一次f(), 输出1
x = 2
f() // 2
x = 3
f() // 3
复制代码
每次调用f
时都会对x解引用取值,因为闭包内保存的是x的地址。
进阶实例
接下来给出一个较为复杂的例子,涉及到了闭包的循环引用,借此可以加深对闭包的特性的理解:
var funcSlice []func()
for i := 0; i < 3; i++ {
funcSlice = append(funcSlice, func() {
println(i)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]()
}
复制代码
读者可以先思考一下这段代码最后会输出什么。
5
4
3
2
1
0
这段代码最后的输出结果为3 3 3
。
感觉有点不理解了?这是正常的,让我们分析一下这段代码。
首先是对funcSlice
这个slice变量的声明,通过一个for循环,每次都在其末尾添加一个闭包,闭包内是将变量i
输出。
看到这个闭包的定义,是不是与之前那个输出x
的函数一样?所以这里的i
也是一个引用,闭包内实际上保存着的是i
的地址,只不过在调用闭包函数时,自动解引用取了其保存的值,可以这样修改这段代码:
var funcSlice []func()
for i := 0; i < 3; i++ {
funcSlice = append(funcSlice, func() {
println(&i)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]()
}
复制代码
看到的输出结果为0xc0000ac1d0 0xc0000ac1d0 0xc0000ac1d0
,即每一次声明这个闭包得到的i
的地址是一样的,故最后i
的输出结果都是第一个for循环结束后i
被赋予的值3
。
那么如果要让输出结果为1 2 3
有什么方法呢?下面给出两种解决方案,这两种解决方案也是遇到大多数闭包引用问题时可以使用的方法。
一、声明新变量
在返回的闭包内声明新变量:Output := i
,并输出j
,这样输出的就不再是对i
的引用而是Output
的值。修改后的代码为:
var funcSlice []func()
for i := 0; i < 3; i++ {
funcSlice = append(funcSlice, func() {
Output := i
println(Output)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]()
}
复制代码
二、声明新函数并传参
将代码修改为:
var funcSlice []func()
for i := 0; i < 3; i++ {
func(i int) {
funcSlice = append(funcSlice, func() {
println(i)
}) //闭包
}(i) //闭包并调用
}
for j := 0; j < 3; j++ {
funcSlice[j]() //调用
}
复制代码
现在 println(i)
使用的 i
是通过函数参数传递进来的,并且 Go 语言的函数参数是按值传递的。
所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。
思考题
希望通过本篇博客读者能对Go语言中的闭包有一定的掌握。
可以自行尝试使用闭包是实现一个Fibonacci函数,其返回值为一个闭包,能返回连续的Fibonacci数。