閉包定義
閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體(即: 閉包=函數(shù)+引用環(huán)境)
閉包知識點(diǎn)
Go語言支持閉包
Go語言能通過escape analyze識別出變量的作用域芹务,自動將變量在堆上分配浑厚。將閉包環(huán)境變量在堆上分配是Go實(shí)現(xiàn)閉包的基礎(chǔ)住诸。
返回閉包時并不是單純返回一個函數(shù)啸如,而是返回了一個結(jié)構(gòu)體薄嫡,記錄下函數(shù)返回地址和引用的環(huán)境中的變量地址。
閉包結(jié)構(gòu)體
閉包結(jié)構(gòu)體
回到閉包的實(shí)現(xiàn)來吭净,前面說過替蔬,閉包是函數(shù)和它所引用的環(huán)境。那么是不是可以表示為一個結(jié)構(gòu)體呢:
type Closure struct {
F func()()
i *int
}
事實(shí)上相嵌,Go在底層確實(shí)就是這樣表示一個閉包的挽荠。讓我們看一下匯編代碼:
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) // 這個結(jié)構(gòu)體就是閉包的類型
...
CALL ,runtime.new(SB) // 接下來相當(dāng)于 new(Closure)
PCDATA $0,$-1
MOVQ 8(SP),AX
NOP ,
MOVQ $"".func·001+0(SB),BP
MOVQ BP,(AX) // 函數(shù)地址賦值給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是另一個函數(shù)的函數(shù)地址,也就是f返回的那個函數(shù)圈匆。
考點(diǎn)
考點(diǎn)1:閉包引用環(huán)境
func f(i int) func() int {
return func() int {
i++
return i
}
}
函數(shù)f返回了一個函數(shù),返回的這個函數(shù)捏雌,返回的這個函數(shù)就是一個閉包跃赚。這個函數(shù)中本身是沒有定義變量i的,而是引用了它所在的環(huán)境(函數(shù)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引用的是不同的環(huán)境纬傲,在調(diào)用i++時修改的不是同一個i,因此兩次的輸出都是1肤频。函數(shù)f每進(jìn)入一次叹括,就形成了一個新的環(huán)境,對應(yīng)的閉包中宵荒,函數(shù)都是同一個函數(shù)汁雷,環(huán)境卻是引用不同的環(huán)境。
考點(diǎn)2: 閉包在循環(huán)語句中的引用環(huán)境
- 例1
for i := 0; i < 3; i++ {
func() {
println(i)
}()
}
解答:
這段代碼相當(dāng)于
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
}
復(fù)制代碼然而這段代碼會輸出 3:
var dummy [3]int
var f func()
for i := 0; i < len(dummy); i++ {
f = func() {
println(i)
}
}
f() // 3
把循環(huán)轉(zhuǎn)換成這樣的形式就容易理解了:
var dummy [3]int
var f func()
for i := 0; i < len(dummy); {
f = func() {
println(i)
}
i++
}
f() // 3
復(fù)制代碼i 自加到 3 才會跳出循環(huán)报咳,所以循環(huán)結(jié)束后 i 最后的值為 3
所以用 for range 來實(shí)現(xiàn)這個例子就不會這樣:
var dummy [3]int
var f func()
for i := range dummy {
f = func() {
println(i)
}
}
f() // 2
復(fù)制代碼這是因?yàn)?for range 和 for 底層實(shí)現(xiàn)上的不同侠讯。
考點(diǎn)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
}
復(fù)制代碼輸出序列為 3, 3, 3。
看了前面的例子之后這里就容易理解了:
這三個函數(shù)引用的都是同一個變量(i)的地址暑刃,所以之后 i 遞增厢漩,解引用得到的值也會遞增,所以這三個函數(shù)都會輸出 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
}
現(xiàn)在 println(i) 使用的 i 是通過函數(shù)參數(shù)傳遞進(jìn)來的溜嗜,并且 Go 語言的函數(shù)參數(shù)是按值傳遞的。
所以相當(dāng)于在這個新的匿名函數(shù)內(nèi)聲明了三個變量架谎,被三個閉包函數(shù)獨(dú)立引用炸宵。原理跟第一種方法是一樣的。
這里的解決方法可以用在大多數(shù)跟閉包引用有關(guān)的問題上
參考博客
https://juejin.im/post/5c850d035188257ec629e73e#heading-5
https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.6.html