闭包
我们知道在GO语言中,函数是一等公民。它既可以作为参数,也可以作为函数的返回值返回。闭包通俗地说就是可以访问外部函数内部变量的函数。它通过存储相应的函数和捕获的变量来实现。
在讲闭包之前,我们先学习一下Go语言中的函数。Go 语言中Function value本质上是一个指针,但是其并不直接指向函数的入口地址,而是指向的runtime.funcval( runtime/runtime2.go)这个结构体。该结构体中的fn字段存储的是函数的入口地址。以下面这段代码为例:
func A(i int) {
i++
fmt.Println(i)
}
func B() {
f1 := A
f1(1)
}
func C() {
f2 := A
f2(2)
}
它在内存中的布局如下:
f1和f2并不直接保存函数的地址。而是指向fn,fn结构体中才保存着相应的函数指针。之所以通过二级指针来指向函数是因为闭包的设计。因为闭包是有状态的。它不仅需要保存相应的函数,还需要保存捕获的变量。
以下函数举例:
package main
func A() func() int {
i := 3
return func() int {
return i
}
}
func main() {
f1 := A()
f2 := A()
print(f1())
print(f2())
}
当main函数执行,它会在main函数的栈区分配f1、f2变量,它指向保存在堆区的funcval结构体,同时紧挨着该结构体就有该闭包函数捕获的变量。这也就解释了为什么要使用二级指针来保存了。
通过查看代码编译期间的变量逃逸情况,可以发现是符合我们的分析的:
标红的那一行代码就是闭包函数
return那句。
需要注意的是,当捕获外部函数的内部变量且该变量会发生改变的时候,该变量就会逃逸到堆上。
package main
func app() func(string) string {
t := "Hi"
c := func(b string) string {
t = t + " " + b
return t
}
return c
}
func main() {
a := app()
b := app()
a("go")
b("All")
}
大家可以自行通过编译语句验证。
闭包和协程
协程的开启是异步的且需要一定时间。在go1.23之前,下面的代码都只会打印:3, 3, 3
package main
import (
"fmt"
"time"
)
func main() {
for i := 0; i < 3; i++ {
go func() {
fmt.Println(i)
}()
}
time.Sleep(1 * time.Second)
}
当协程开启的时候,这时候循环变量已经变成3了。我们可以通过传递参数或者对捕获变量进行拷贝来解决该问题。