大家好,今天是鐵人賽第十六天。還記得 day5-常數與函式 講的函式用法嗎? go語言的函式是可以當作變數使用,而且函式也是一種型別,今天我要講來函式的一種進階用法 - 閉包。
你或許會想問我,既然是函式相關的用法,為什麼要拖個10天才講? 因為我覺得閉包很難理解,而且實務上我沒有用過,我只有執行過簡單的範例。因此,我選擇把閉包排在物件導向的後面,因為我覺得閉包很像物件,它也具有封裝的能力。
回想起我第一次聽到閉包時,是在學JavaScript的時候,聽說前端在處理事件時,很常用到閉包的技巧,而因為我是後端人,所以自然也就沒有深入研究了。另外,因為JavaScript物件沒有私有屬性的特性,所以就會使用閉包來保護內部的資料。
並非所有語言都有閉包,閉包最大的形成條件就在於,函式是否為一級成員(first class)。因此,go語言確實擁有閉包特性,要形成閉包的因素如下:
範例:
var i int
var foo = func() {
i++
fmt.Println(i)
}
foo()
foo()
foo()
// 執行結果:
// 1
// 2
// 3
上面的匿名函式引用外部變數 i 形成閉包,每次執行時會修改同一份資料,這是因為go語言的作用域,讓外部的環境變數能被匿名函式捕獲到。
由於閉包捕獲的外部變數都是同一個,因此代表閉包是具有記憶性的,我們還可以讓外部變數不被其他人修改,如下:
var foo = func() func() {
var i int
return func() {
i++
fmt.Println(i)
}
}()
foo()
foo()
foo()
// 執行結果:
// 1
// 2
// 3
這段程式有點複雜,foo 是一個回傳函式的函式,講起來好饒舌。然後, foo 函式裡宣告區域變數 i ,以及回傳一個前面範例中的函式。執行結果和前面範例相同,但為什麼要這麼做?
因為區域變數 i 不會被 GC,它被 foo 函式變數裡的閉包關住了,原本外部的匿名函式在執行後應該要被回收的,但是因為回傳的函式中使用到區域變數 i,而回傳函式又被 foo 變數持有,導致外部函式無法被回收,延長了區域變數 i 的生命週期。
閉包可以做很類似物件導向的事,可以同時封裝資料和行為,在go語言中就和結構很相似。下面舉一個範例說明,閉包不一定要回傳函式,也可以回傳結構。
我們先定義一個結構型別:
// 定義一個計數器的結構型別,擁有 3 個函式屬性
type counter struct {
add func()
minus func()
print func()
}
然後建立閉包:
count := func() counter {
i := 0
return counter{
func() {
i++
},
func() {
i--
},
func() {
fmt.Println("i =", i)
},
}
}()
count.add()
count.add()
count.minus()
count.print()
// 執行結果:
// i = 1
上面範例透過回傳結構值中的函式變數來形成閉包,count 只能用 add 和 minus 修改資料,以及用 print 印出值。
用這樣方式產生的結構實體和前幾天講的結構完全不同,不需要定義資料欄位和方法,就可以達到相同的效果,而且可以完全保護內部的資料,就像是結構加上介面的組合。
今天簡單介紹了閉包特性,以及go語言中如何使用閉包,之後在併發的篇幅中應該還會再提到。我現在對於閉包的特性也還不算完全了解,以上的內容如果有錯,歡迎留言告訴我。今天就先到這裡了,明天見喔。