閉包是主流編程語言中的一種通用技術,經常和函數式編程進行強強聯合,本文主要是介紹 Go
語言中什麼是閉包以及怎麼理解閉包.html
若是讀者對於 Go
語言的閉包還不是特別清楚的話,能夠參考上一篇文章 go 學習筆記之僅僅須要一個示例就能講清楚什麼閉包.linux
或者也能夠直接無視,由於接下來會回顧一下前情概要,如今你準備好了嗎? Go
!git
不管是 Go
官網仍是網上其餘講解閉包的相關教程,總能看到斐波那契數列的身影,足以說明該示例的經典!github
斐波那契數列(
Fibonacci sequence
),又稱黃金分割數列 .因數學家列昂納多·斐波那契(Leonardoda Fibonacci
)以兔子繁殖爲例子而引入,故又稱爲「兔子數列」,指的是這樣一個數列:一、一、二、三、五、八、1三、2一、3四、……
在數學上,斐波那契數列以以下被以遞推的方法定義:F(1)=1,F(2)=1, F(n)=F(n-1)+F(n-2)(n>=3,n∈N*)
.在現代物理、準晶體結構、化學等領域,斐波納契數列都有直接的應用,爲此,美國數學會從1963年起出版了以《斐波納契數列季刊》爲名的一份數學雜誌,用於專門刊載這方面的研究成果.編程
根據上述百度百科的有關描述,咱們知道斐波那契數列就是形如 1 1 2 3 5 8 13 21 34 55
的遞增數列,從第三項開始起,當前項是前兩項之和.數組
爲了計算方便,定義兩個變量 a,b
表示前兩項,初始值分別設置成 0,1
,示例:緩存
// 0 1 1 2 3 5 8 13 21 34 55
// a b
// a b
a, b := 0, 1
複製代碼
初始化後下一輪移動,a, b = b, a+b
結果是 a , b = 1 , 1
,恰好可以表示斐波那契數列的開頭.閉包
「雪之夢技術驛站」試想一下: 若是
a,b
變量的初始值是1,1
,不更改邏輯的狀況下,最終生成的斐波那契數列是什麼樣子?app
func fibonacciByNormal() {
a, b := 0, 1
a, b = b, a+b
fmt.Print(a, " ")
fmt.Println()
}
複製代碼
可是上述示例只能生成斐波那契數列中的第一個數字,假如咱們須要前十個數列,又該如何?編程語言
func fibonacciByNormal() {
a, b := 0, 1
for i := 0; i < 10; i++ {
a, b = b, a+b
fmt.Print(a, " ")
}
fmt.Println()
}
複製代碼
經過指定循環次數再稍加修改上述單數列代碼,如今就能夠生成前十位數列:
// 1 1 2 3 5 8 13 21 34 55
func TestFibonacciByNormal(t *testing.T) {
fibonacciByNormal()
}
複製代碼
這種作法是接觸閉包概念前咱們一直在採用的解決方案,相信稍微有必定編程經驗的開發者都能實現,可是閉包卻提供了另外一種思路!
// 1 1 2 3 5 8 13 21 34 55
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
複製代碼
不管是普通函數仍是閉包函數,實現斐波那契數列生成器函數的邏輯不變,只是實現不一樣,閉包返回的是內部函數,留給使用者繼續調用而普通函數是直接生成斐波那契數列.
// 1 1 2 3 5 8 13 21 34 55
func TestFibonacci(t *testing.T) {
f := fibonacci()
for i := 0; i < 10; i++ {
fmt.Print(f(), " ")
}
fmt.Println()
}
複製代碼
對於這種函數內部嵌套另外一個函數而且內部函數引用了外部變量的這種實現方式,稱之爲"閉包"!
「雪之夢技術驛站」: 閉包是函數+引用環境組成的有機總體,二者缺一不可,詳細請參考go 學習筆記之僅僅須要一個示例就能講清楚什麼閉包.
「雪之夢技術驛站」: 自帶運行環境的閉包正如電影中出場自帶背景音樂的發哥同樣,音樂響起,發哥登場,閉包出現,環境自帶!
閉包自帶獨立的運行環境,每一次運行閉包的環境都是相互獨立的,正如面向對象中類和對象實例化的關係那樣,閉包是類,閉包的引用是實例化對象.
func autoIncrease() func() int {
i := 0
return func() int {
i = i + 1
return i
}
}
複製代碼
上述示例是閉包實現的計算器自增,每一次引用 autoIncrease
函數得到的閉包環境都是彼此獨立的,直接上單元測試用例.
func TestAutoIncrease(t *testing.T) {
a := autoIncrease()
// 1 2 3
t.Log(a(), a(), a())
b := autoIncrease()
// 1 2 3
t.Log(b(), b(), b())
}
複製代碼
函數引用 a
和 b
的環境是獨立的,至關於另外一個如出一轍計數器從新開始計數,並不會影響原來的計數器的運行結果.
「雪之夢技術驛站」: 閉包不只僅是函數,更加劇要的是環境.從運行效果上看,每一次引用閉包函數從新初始化運行環境這種機制,很是相似於面向對象中類和實例化對象的關係!
普通函數內部定義的變量壽命有限,函數運行結束後也就被系統銷燬了,結束了本身短暫而又光榮的一輩子.
可是,閉包所引用的變量卻不同,只要一直處於使用中狀態,那麼變量就會"長生不老",並不會由於出身於函數內就和普通變量擁有同樣的短暫人生.
func fightWithHorse() func() int {
horseShowTime := 0
return func() int {
horseShowTime++
fmt.Printf("(%d)祖國須要我,我就提槍上馬當即戰鬥!\n",horseShowTime)
return horseShowTime
}
}
func TestFightWithHorse(t *testing.T) {
f := fightWithHorse()
// 1 2 3
t.Log(f(), f(), f())
}
複製代碼
「雪之夢技術驛站」: 若是使用者一直在使用閉包函數,那麼閉包內部引用的自由變量就不會被銷燬,一直處於活躍狀態,從而得到永生的超能力!
凡事有利必有弊,閉包不死則引用變量不滅,若是不理解變量長生不老的特性,編寫閉包函數時可能一不當心就掉進做用域陷阱了,千萬要當心!
下面以綁定循環變量爲例講解閉包做用域的陷阱,示例以下:
func countByClosureButWrong() []func() int {
var arr []func() int for i := 1; i <= 3; i++ {
arr = append(arr, func() int {
return i
})
}
return arr
}
複製代碼
countByClosureButWrong
閉包函數引用的自由變量不只有 arr
數組還有循環變量 i
,函數的總體邏輯是: 閉包函數內部維護一個函數數組,保存的函數主要返回了循環變量.
func TestCountByClosure(t *testing.T) {
// 4 4 4
for _, c := range countByClosureButWrong() {
t.Log(c())
}
}
複製代碼
當咱們運行 countByClosureButWrong
函數得到閉包返回的函數數組 arr
,而後經過 range
關鍵字進行遍歷數組,獲得正在遍歷的函數項 c
.
當咱們運行 c()
時,指望輸出的 1,2,3
循環變量的值,可是實際結果倒是 4,4,4
.
緣由仍然是變量長生不老的特性:遍歷循環時綁定的變量值確定是 1,2,3
,可是循環變量 i
卻沒有像普通函數那樣消亡而是一直長生不老,因此變量的引用發生變化了!
長生不老的循環變量的值恰好是當初循環的終止條件 i=4
,只要運行閉包函數,不管是數組中的哪一項函數引用的都是相同的變量 i
,因此所有都是 4,4,4
.
既然是變量引用出現問題,那麼解決起來就很簡單了,不用變量引用就行了嘛!
最簡單的作法就是使用短暫的臨時變量 n
暫存起來正在遍歷的值,閉包內引用的變量再也不是 i
而是臨時變量 n
.
func countByClosureButWrong() []func() int {
var arr []func() int for i := 1; i <= 3; i++ {
n := i
fmt.Printf("for i=%d n=%d \n", i,n)
arr = append(arr, func() int {
fmt.Printf("append i=%d n=%d\n", i, n)
return n
})
}
return arr
}
複製代碼
上述解決辦法很簡單就是採用臨時變量綁定循環變量的值,而不是原來的長生不老的變量引用,可是這種作法不夠優雅,還能夠繼續簡化進行版本升級.
既然是採用變量賦值的作法,是否是和參數傳遞中的值傳遞很相像?那咱們就能夠用值傳遞的方式從新複製一份變量的值傳遞給閉包函數.
func countByClosureWithOk() []func() int {
var arr []func() int for i := 1; i <= 3; i++ {
fmt.Printf("for i=%d \n", i)
func(n int) {
arr = append(arr, func() int {
fmt.Printf("append n=%d \n", n)
return n
})
}(i)
}
return arr
}
複製代碼
「雪之夢技術驛站」: 採用匿名函數自執行的方式傳遞參數
i
,函數內部使用變量n
綁定了外部的循環變量,看起來更加優雅,有逼格!
採用匿名函數進行值傳遞進行改造後,咱們再次運行測試用例驗證一下改造結果:
func TestCountByClosureWithOk(t *testing.T) {
// 1 2 3
for _, c := range countByClosureWithOk() {
t.Log(c())
}
}
複製代碼
終於解決了正確綁定循環變量的問題,下次再出現實際結果和預期不符,不必定是 bug
有多是理解不深,沒有正確使用閉包!
「雪之夢技術驛站」: 每次調用閉包函數所處的環境都是相互獨立的,這種特性相似於面向對象中類和實例化對象的關係.
「雪之夢技術驛站」: 長生不老的特性使得閉包引用變量能夠常駐內存,用於緩存一些複雜邏輯代碼很是合適,避免了原來的全局變量的濫用.
「雪之夢技術驛站」: 普通函數轉變成閉包函數不只實現起來有必定難度,並且理解起來也不容易,不只要求多測試幾遍還要理解閉包的特性.
「雪之夢技術驛站」: 過多使用閉包勢必形成引用變量一直常駐內存,若是出現循環引用或者垃圾回收不及時有可能形成內存泄漏問題.
閉包是一種通用技術,Go
語言支持閉包,主要體如今 Go
支持函數內部嵌套匿名函數,但 Go
不支持普通函數嵌套.
簡單的理解,閉包是函數和環境的有機結合總體,獨立和運行環境和長生不老的引用變量是閉包的兩大重要特徵.
不管是模擬面向對象特性,實現緩存仍是封裝對象等等應用都是這兩特性的應用.
最後,讓咱們再回憶一下貫穿始終的斐波那契數列來結束這次閉包之旅!
func fibonacci() func() int {
a, b := 0, 1
return func() int {
a, b = b, a+b
return a
}
}
複製代碼
本文涉及示例代碼: github.com/snowdreams1…
若是你以爲本文對你有所幫助,歡迎點贊留言告訴我,你的鼓勵是我繼續創做的動力,不妨順便關注下我的公衆號「雪之夢技術驛站」,按期更新優質文章喲!