闭包定义
闭包是由函数和与其相关的引用环境组合而成的实体(即: 闭包=函数+引用环境)
闭包知识点
Go语言支持闭包
Go语言能通过escape analyze识别出变量的作用域,自动将变量在堆上分配。将闭包环境变量在堆上分配是Go实现闭包的基础。
返回闭包时并不是单纯返回一个函数,而是返回了一个结构体,记录下函数返回地址和引用的环境中的变量地址。
闭包结构体
闭包结构体
回到闭包的实现来,前面说过,闭包是函数和它所引用的环境。那么是不是可以表示为一个结构体呢:
type Closure struct {
F func()()
i *int
}
事实上,Go在底层确实就是这样表示一个闭包的。让我们看一下汇编代码:
func f(i int) func() int {
return func() int {
i++
return i
}
}
MOVQ $type.int+0(SB),(SP)
PCDATA $0,$16
PCDATA $1,$0
CALL ,runtime.new(SB) // 是不是很熟悉,这一段就是i = new(int)
...
MOVQ $type.struct { F uintptr; A0 *int }+0(SB),(SP) // 这个结构体就是闭包的类型
...
CALL ,runtime.new(SB) // 接下来相当于 new(Closure)
PCDATA $0,$-1
MOVQ 8(SP),AX
NOP ,
MOVQ $"".func·001+0(SB),BP
MOVQ BP,(AX) // 函数地址赋值给Closure的F部分
NOP ,
MOVQ "".&i+16(SP),BP // 将堆中new的变量i的地址赋值给Closure的值部分
MOVQ BP,8(AX)
MOVQ AX,"".~r1+40(FP)
ADDQ $24,SP
RET ,
其中func·001是另一个函数的函数地址,也就是f返回的那个函数。
考点
考点1:闭包引用环境
func f(i int) func() int {
return func() int {
i++
return i
}
}
函数f返回了一个函数,返回的这个函数,返回的这个函数就是一个闭包。这个函数中本身是没有定义变量i的,而是引用了它所在的环境(函数f)中的变量i。
c1 := f(0)
c2 := f(0)
c1() // reference to i, i = 0, return 1
c2() // reference to another i, i = 0, return 1
c1跟c2引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数f每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。
考点2: 闭包在循环语句中的引用环境
- 例1
for i := 0; i < 3; i++ {
func() {
println(i)
}()
}
解答:
这段代码相当于
for i := 0; i < 3; i++ {
f := func() {
println(i)
}
f()
}
这样就很清楚的能看出来最后的输出为 0,1,2
- 例2
正常代码:输出 0, 1, 2:
var dummy [3]int
for i := 0; i < len(dummy); i++ {
println(i) // 0, 1, 2
}
复制代码然而这段代码会输出 3:
var dummy [3]int
var f func()
for i := 0; i < len(dummy); i++ {
f = func() {
println(i)
}
}
f() // 3
把循环转换成这样的形式就容易理解了:
var dummy [3]int
var f func()
for i := 0; i < len(dummy); {
f = func() {
println(i)
}
i++
}
f() // 3
复制代码i 自加到 3 才会跳出循环,所以循环结束后 i 最后的值为 3
所以用 for range 来实现这个例子就不会这样:
var dummy [3]int
var f func()
for i := range dummy {
f = func() {
println(i)
}
}
f() // 2
复制代码这是因为 for range 和 for 底层实现上的不同。
考点3: 闭包列表
- 例1
var funcSlice []func()
for i := 0; i < 3; i++ {
funcSlice = append(funcSlice, func() {
println(i)
})
}
for j := 0; j < 3; j++ {
funcSlice[j]() // 3, 3, 3
}
复制代码输出序列为 3, 3, 3。
看了前面的例子之后这里就容易理解了:
这三个函数引用的都是同一个变量(i)的地址,所以之后 i 递增,解引用得到的值也会递增,所以这三个函数都会输出 3。
- 例2
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]() // 0, 1, 2
}
现在 println(i) 使用的 i 是通过函数参数传递进来的,并且 Go 语言的函数参数是按值传递的。
所以相当于在这个新的匿名函数内声明了三个变量,被三个闭包函数独立引用。原理跟第一种方法是一样的。
这里的解决方法可以用在大多数跟闭包引用有关的问题上
参考博客
https://juejin.im/post/5c850d035188257ec629e73e#heading-5
https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.6.html