閉包是一種可以讓你用很是舒服的方式來編程的小技巧,Go也支持閉包。html
假設歷來沒有接觸過閉包,想在一開始就弄懂什麼是閉包(closure)是很是困難的,就像遞歸同樣,直到你真正寫過、用過它,你才幹真正的對它有一個更詳細的認識。java
閉包就是一個函數,這個函數包括了執行它所需的上下文環境,這個環境多是幾個變量或者也會是其它的(一般就是變量)。說閉包是一個函數不對,更確切地說。閉包是一個打包了其做用域外部的上下文環境的一段執行環境。假設一時間沒有理解這段閉包的含義也沒關係。這是一個按部就班的過程。編程
那麼咱們來看一個網上最通用的解說閉包的樣例:閉包
package main import "fmt" func A() func() int { value := 0 return func() int { value++ return value } // A()到這裏時按理說應該已經銷燬掉了局部變量value } func main() { B := A() fmt.Println(B()) fmt.Println(B()) fmt.Println(B()) }
執行結果爲:
併發
1 2 3好好看下這段代碼,咱們定義一個函數A(),在A中咱們又定義了還有一個函數B(),而且B將會做爲返回值返回給咱們。B的做用就是每次調用都會給A的局部變量value增1。
但是當A函數執行完以後。按理說局部變量value應該已經被銷燬了,B沒法再對其進行操做,但是從執行結果來看。value卻還"活着"。沒錯,這就是所謂的打包了上下文環境的函數,B就是一個閉包函數,它不只定義了本身的操做流程,而且附帶着它的上文環境A中的value變量。編程語言
從這個樣例你可能沒太看出閉包有什麼做用。但是事實上閉包這個東西或多或少會要求編程語言支持匿名函數而且函數在該語言中式第一類型(可以把函數賦值給變量,可以用函數當參數和返回值)。閉包的做用我也不大好說。像JS這種缺陷比較多的語言使用閉包可以帶來保護變量的訪問權限、更好的模塊化這種優勢,而對於Go,我認爲閉包最大的優勢就是:模塊化
1. 不用特地給某個函數取名字了。省事兒~函數
2. 可以把某個暫時使用的函數定義在近期的地方post
3. 和goroutine使用的時候可以直接用go func() {...}() 這種寫法。這就不用把每個要併發的函數都在當前執行環境的外部預先定義一遍了。spa
講完了閉包的基本內容。接下來就講一講使用閉包時應該注意的問題了。
原則上講,閉包用起來的最大優勢就是方便,但是不要在不論什麼狀況下都首先想到使用閉包,因爲假設你對閉包缺少了解。那你寫出的代碼很是可能會有意外的執行效果。來看一下如下的樣例:
package main import ( "fmt" ) func main() { done := make(chan bool, 3) for i := 0; i < 3; i++ { go func() { //這裏是有問題的。每個routine都打包了外層執行環境中的變量i fmt.Println(i) done <- true }() } for i := 0; i < 3; i++ { <-done } }
執行結果是:
3 3 3假設你細緻看下代碼的話,應該會認爲代碼看上去沒有不論什麼問題。但是結果爲甚不是1 2 3呢???事實上Go的官方指南也給出了相關的樣例讓開發人員們注意閉包和goroutine一塊兒使用時要注意的這個問題。
這個樣例中事實上第一個循環中的三個goroutine在建立以後都不會立馬執行,因爲在他們都綁定了外層執行環境中的變量i,因爲外層的執行環境隨時會更改i的值。因此知道這個循環結束。三個routine都不能開始執行。
這時的解決的方法就是把要用到的外層上下文環境做爲參數傳遞給閉包函數。像這樣:
package main import ( "fmt" ) func main() { done := make(chan bool, 3) for i := 0; i < 3; i++ { go func(index int) { // 把外層環境中的i當作參數傳進來 fmt.Println(index) done <- true }(i) // 把i傳進去 } for i := 0; i < 3; i++ { <-done } }這種話閉包函數就不需要再綁定外層環境中的那個變量i了。這個問題必定要注意,因爲Go裏會經常將goroutine和閉包配合使用,假設沒有處理好上下文打包的問題,就很是可能引起意外的執行效果。