閉包(closure)與協程共用時要註意的事情
閉包是一種能夠讓你用非常舒服的方式來編程的小技巧,Go也支持閉包。
假設從來沒有接觸過閉包,想在一開始就弄懂什麽是閉包(closure)是非常困難的,就像遞歸一樣,直到你真正寫過、用過它,你才幹真正的對它有一個更詳細的認識。
閉包就是一個函數,這個函數包括了執行它所需的上下文環境,這個環境可能是幾個變量或者也會是其它的(通常就是變量)。說閉包是一個函數不對,更確切地說。閉包是一個打包了其作用域外部的上下文環境的一段執行環境。假設一時間沒有理解這段閉包的含義也不要緊。這是一個循序漸進的過程。
那麽我們來看一個網上最通用的解說閉包的樣例:
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. 能夠把某個暫時使用的函數定義在近期的地方
3. 和goroutine使用的時候能夠直接用go func() {...}() 這種寫法。這就不用把每一個要並發的函數都在當前執行環境的外部預先定義一遍了。
講完了閉包的基本內容。接下來就講一講使用閉包時應該註意的問題了。
原則上講,閉包用起來的最大優點就是方便,可是不要在不論什麽情況下都首先想到使用閉包,由於假設你對閉包缺乏了解。那你寫出的代碼非常可能會有意外的執行效果。來看一下以下的樣例:
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和閉包配合使用,假設沒有處理好上下文打包的問題,就非常可能引發意外的執行效果。
假設轉載,請註明出處:http://blog.csdn.net/gophers
閉包(closure)與協程共用時要註意的事情