闭包¶
原理¶
我们先来看这样一个示例:
func test() func() {
x := 100
return func() {
println(x)
}
}
func main() {
closure := test()
closure()
}
closure := test()
时,test的栈帧会被销毁,局部变量x应该失效,按照常理,匿名函数应该找不到x而无法打印,但实际上运行之后是能够正常打印的。那么这个x在哪?只有一种可能,它逃逸到堆上去了。既然是在堆上,x就会有一个地址,问题就变为匿名函数是怎么知道x的地址的?
我们把这种现象叫做闭包,当一个匿名函数离开了它的宿主时,它依然持有它所引用的环境变量。所以闭包是包含两个部分的,其一是一个指针指向匿名函数的地址,其二是所拥有的环境变量,这两个东西打包合起来才称为一个闭包。
可以理解为test函数的返回用伪码来表达应该是:
只不过Go的语法上让我们可以简写为示例代码中的样子,调用的时候编译器也是隐式的使用closure.f(closure.v)
这样的方式来调用。
实现¶
下面我们观察它的编译过程来证明这件事。使用go build -gcflags "-S" 2>a.txt closure.go
把编译过程放到一个文本文件里,从中能找到一些相关线索:
"".x+16(SP)
的位置现在也转到了AX中存的地址+8的位置,也就是说现在在给之前的结构体填充数据。最终把AX中的内容当做返回值返回,~r0
通常表示返回值,伪码表示test函数实际上返回的就是return &struct{F:test.func1, x:100}
。
那么在main函数中也就会接收到这样一个地址的返回值。因为test函数没有参数,SP偏移量为0的地址就是其返回值的地址,第12行就说明把test的返回放在了DX中;接着通过第14行把test返回值的地址也就是test.func1
返回给AX寄存器,并在第15行调用它。
在test.func1
中,通过第18行从DX中存的地址偏移量+8也就是x的值100放在了AX寄存器中,接着放到栈上执行print。
DX寄存器是整个闭包实现的核心,因为结构体是在堆上运行期动态分配的,它的地址在编译期不知道,闭包中的函数也是编译期就被编译为指令的,那么函数执行时如何知道它的本地变量的内存地址就是问题。DX寄存器是一个名字,编译期、运行期都一样,Go把动态的地址写进去,通过这个静态的名字加上偏移量来交互实现闭包。
应用场景¶
计数器¶
每次调用返回一个自增的值,以前可能通过类或者全局变量,用函数实现会比较麻烦,有了闭包会比较简单:
func getCounter() func() int {
x := 0
return func() int {
x++
return x
}
}
func main() {
c := getCounter()
println(c())
println(c())
println(c())
}
封闭区间¶
也可以把一份数据绑定到多个函数中去,如下例:
func test() (func(), func(), func()) {
x := 100
fmt.Printf("%v\n", &x)
return func() {
fmt.Printf("f1.x:%v\n", &x)
}, func() {
fmt.Printf("f2.x:%v\n", &x)
}, func() {
fmt.Printf("f3.x:%v\n", &x)
}
}
func main() {
f1, f2, f3 := test()
f1()
f2()
f3()
}
绑定数据和逻辑¶
有时候需要A把方法和不同数据都传给B,直接传看上去不优雅,把它们打包为一个闭包看上去就像只传了一个方法,B只是接收一套逻辑,这个逻辑具体是怎么运行B不需要知道。
func hello(db Database) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, db.Url)
}
}
func main() {
db := NewDatabase("localhost:5432")
http.HandleFunc("/hello", hello(db))
http.ListenAndServe(":3000", nil)
}
改变函数签名¶
例如把一个函数变成偏函数(partial)
func test(x int) {
println(x)
}
func partial(f func(int), x int) func() {
return func() {
f(x)
}
}
func main() {
f := partial(test, 100)
f()
}
总结¶
闭包虽然改善了一些代码上的设计,让代码看上去更干净整洁,可以实现一些设计模式。但它延长了变量的生命周期,必然会导致逃逸行为,其内部的一些交互过程也会使执行性能降低。所以对于闭包也不能滥用。