定義
函式可以巢狀定義(巢狀的函式一般為匿名函式),即在一個函式內部可以定義另一個函式。Go語言通過匿名函式支援閉包,C++不支援匿名函式,在C++11中通過Lambda表示式支援閉包。
閉包是由函式及其相關引用環境組合而成的實體(即:閉包=函式+引用環境
)。
引用環境的定義:
在函式式語言中,當內嵌函式體內引用到體外的變數時,將會把定義時涉及到的引用環境和函式體打包成一個整體(閉包)返回。當每次呼叫包含閉包的函式時,都將返回一個新的閉包例項,這些例項之間是隔離的,分別包含呼叫時不同的引用環境現場。
不同於函式,閉包在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項
。
關鍵知識點
-
閉包與逃逸分析
閉包可能會導致變數逃逸到堆上來延長變數的生命週期,給GC帶來壓力
func closure() func(int) int{ var x int return func(a int) int { x++ return a + x } } func main() { a := closure() //x逃逸到堆上,成為a閉包例項的一部分 fmt.Println(a(1)) //2 fmt.Println(a(1)) //3 }
-
閉包與外部函式的生命週期
注意:當closure函式被重新呼叫後,返回的是新的閉包例項,引用的變數也是重新在堆上定義的。
func closure1(n int) func() { n++ return func() { fmt.Println(n) } } func closure2(n int) func() { return func(){ n++ fmt.Println(n) } } func main() { a := closure1(3) //變數被匿名函式引用,closure函式結束,n變數也不會馬上銷燬,而是繫結到了匿名函式上 b := closure2(3) // a() //4 a() //4 b() //4 b() //5 }
-
閉包在for迴圈中問題
func main() { s := []string{"a", "b", "c"} for _, v := range s { go func() { fmt.Println(v) }() } time.Sleep(time.Second * 1) } // c c c
主協程執行完for之後,定義的子協程才開始執行,v最終是c,所以輸出了c c c。如果for過程中,子協程執行了,結果就可能不是c, c,c。
輸出的結果依賴於子協程執行時的那一刻,v是什麼
。func main() { s := []string{"a", "b", "c"} for _, v := range s { go func() { fmt.Println(v) }() time.Sleep(time.Second * 3) } fmt.Println("main routine") time.Sleep(time.Second * 1) // 阻塞模式 } //a b c //還有一種辦法就是需要每次將變數v拷貝給函式即可,但此時就不是使用上下文環境中的變數了 func main() { s := []string{"a", "b", "c"} for _, v := range s { go func(c string) { fmt.Println(c) }(v) //每次將變數 v 的拷貝傳進函式 } select {} }
-
延遲呼叫與閉包
defer中使用匿名函式依然是一個閉包
package main import "fmt" func main() { x, y := 1, 2 defer func(a int) { fmt.Printf("x:%d,y:%d\n", a, y) // y 為閉包引用 }(x) // 複製 x 的值 x += 100 y += 100 fmt.Println(x, y) } /* 101 102 x:1 y:102 */
在defer定義時候已經將x的值1拷貝進了defer函式,defer執行時使用的是defer定義時x的拷貝,而不是當前環境中x的值
總結
其實閉包需要注意的關鍵點就是:當閉包引用外部變數時候,此變數的生命週期就不是它的作用域範圍了,而是被閉包例項捕獲,此時閉包不僅僅和外部函式共享此變數,更重要的是變數的生命週期因匿名函式也就是閉包的存在而延長
。