1. 程式人生 > >go 學習筆記之10 分鐘簡要理解 go 語言閉包技術

go 學習筆記之10 分鐘簡要理解 go 語言閉包技術

閉包是主流程式語言中的一種通用技術,常常和函數語言程式設計進行強強聯合,本文主要是介紹 Go 語言中什麼是閉包以及怎麼理解閉包.

如果讀者對於 Go 語言的閉包還不是特別清楚的話,可以參考上一篇文章 go 學習筆記之僅僅需要一個示例就能講清楚什麼閉包.

或者也可以直接無視,因為接下來會回顧一下前情概要,現在你準備好了嗎? Go !

斐波那契數列見閉包

不論是 Go 官網還是網上其他講解閉包的相關教程,總能看到斐波那契數列的身影,足以說明該示例的經典!

斐波那契數列(Fibonacci sequence),又稱黃金分割數列 .因數學家列昂納多·斐波那契(Leonardoda Fibonacci

)以兔子繁殖為例子而引入,故又稱為“兔子數列”,指的是這樣一個數列: 1、1、2、3、5、8、13、21、34、……在數學上,斐波那契數列以如下被以遞推的方法定義: 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 ,不更改邏輯的情況下,最終生成的斐波那契數列是什麼樣子?

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())
}

函式引用 ab 的環境是獨立的,相當於另一個一模一樣計數器重新開始計數,並不會影響原來的計數器的執行結果.

「雪之夢技術驛站」: 閉包不僅僅是函式,更加重要的是環境.從執行效果上看,每一次引用閉包函式重新初始化執行環境這種機制,非常類似於面向物件中類和例項化物件的關係!

長生不老是福還是禍

普通函式內部定義的變數壽命有限,函式執行結束後也就被系統銷燬了,結束了自己短暫而又光榮的一生.

但是,閉包所引用的變數卻不一樣,只要一直處於使用中狀態,那麼變數就會"長生不老",並不會因為出身於函式內就和普通變數擁有一樣的短暫人生.

  • 老驥伏櫪,志在千里

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
    }
}

本文涉及示例程式碼: https://github.com/snowdreams1006/learn-go/blob/master/functional/closure/closure_test.go

參考資料及延伸閱讀

  • 閉包的概念、形式與應用
  • Jartto 部落格:反思閉包
  • 三點水部落格: 再談閉包
  • gitbook部落格: 閉包的實現

    如果本文對你有所幫助,請動動小手點一下推薦,否則還請留言指正,如有需要,請關注個人公眾號「 雪之夢技術驛站 」

相關推薦

go 學習筆記10 分鐘簡要理解 go 語言技術

閉包是主流程式語言中的一種通用技術,常常和函數語言程式設計進行強強聯合,本文主要是介紹 Go 語言中什麼是閉包以及怎麼理解閉包. 如果讀者對於 Go 語言的閉包還不是特別清楚的話,可以參考上一篇文章 go 學習筆記之僅僅需要一個示例就能講清楚什麼閉包. 或者也可以直接無視,因為接下來會回顧一下前情概要,現在你

Go學習筆記高階資料型別

高階資料型別,僅僅是做個概念認識,等到其他相關知識的學習時,再著重分析。 1 function 將 function 作為資料型別的語言有很多,函數語言程式設計的核心理念。 function 是“第一等公民”,function 與其他資料型別一樣,處於平等地位,可以賦值給

go 學習筆記初識 go 語言

Go 是一種開源程式語言,可以輕鬆構建簡單,可靠,高效的軟體. 摘錄自 github: https://github.com/golang/go,其中官網(國外): https://golang.org 和官網(國內): https://golang.google.cn/ Go 是 Google 公司

go 學習筆記環境搭建

千里之行始於足下,開始 Go 語言學習之旅前,首先要搭建好本地開發環境,然後就可以放心大膽瞎折騰了. Go 的環境安裝和其他語言安

go 學習筆記工作空間

搭建好 Go 的基本環境後,現在可以正式開始 Go 語言的學習之旅,初學時建議在預設的 GOPATH 工作空間規範編寫程式碼,基本目錄結構大概是這個樣子. . |-- bin | `-- hello.exe |-- pkg | `-- windows_amd64 | `-- github.

go 學習筆記走進Goland編輯器

工欲善其事必先利其器,命令列工具雖然能夠在一定程度上滿足基本操作的需求,但實際工作中總不能一直使用命令列工具進行編碼操作吧? 學習 Go 語言同樣如此,為此需要尋找一個強大的 IDE 整合環境幫助我們快速開發,據我所知,市面上比較流行的可能有三個選擇: LiteIDE X : LiteIDE 是一款簡單,開

go 學習筆記有意思的變數和不安分的常量

首先希望學習 Go 語言的愛好者至少擁有其他語言的程式設計經驗,如果是完全零基礎的小白使用者,本教程可能並不適合閱讀或嘗試閱讀看看,系列筆記的目標是站在其他語言的角度學習新的語言,理解 Go 語言,進而寫出真正的 Go 程式. 程式語言中一般都有變數和常量的概念,對於學習新語言也是一樣,變數指的是不同程式語言

go 學習筆記值得特別關注的基礎語法有哪些

在上篇文章中,我們動手親自編寫了第一個 Go 語言版本的 Hello World,並且認識了 Go 語言中有意思的變數和不安分的常量. 相信通過上篇文章的斐波那契數列,你已經初步掌握了 Go 語言的變數和常量與其他主要的程式語言的異同,為了接下來更好的學習和掌握 Go 的基礎語法,下面先簡單回顧一下變數和常量

go 學習筆記陣列還是切片都沒什麼不一樣

上篇文章中詳細介紹了 Go 的基礎語言,指出了 Go 和其他主流的程式語言的差異性,比較側重於語法細節,相信只要稍加記憶就能輕鬆從已有的程式語言切換到 Go 語言的程式設計習慣中,儘管這種切換可能並不是特別順暢,但多加練習尤其是多多試錯,總是可以慢慢感受 Go 語言之美! 在學習 Go 的內建容器前,同樣的,

go 學習筆記go是不是面嚮物件語言是否支援面對物件程式設計?

面向物件程式設計風格深受廣大開發者喜歡,尤其是以 C++, Java 為典型代表的程式語言大行其道,十分流行! 有意思的是這兩中語言幾乎毫無意外都來源於 C 語言,卻不同於 C 的面向過程程式設計,這種面向物件的程式設計風格給開發者帶來了極大的便利性,解放了勞動,鬆耦合,高內聚也成為設計的標準,從而讓我們

go 學習筆記詳細說一說封裝是怎麼回事

關注公眾號[雪之夢技術驛站]檢視上篇文章 猜猜看go是不是面嚮物件語言?能不能面向物件程式設計? 雖然在上篇文章中,我們通過嘗試性學習探索了 Go 語言中關於面向物件的相關概念,更確切的說是關於封裝的基本概念以及相關實現. 但那還遠遠不夠,不能滿足於一條路,而是應該儘可能地多走幾條路,只有這樣才能為以後可

go 學習筆記是否支援以及如何實現繼承

熟悉面向物件的小夥伴們可能會知道封裝,繼承和多型是最主要的特性,為什麼前輩們會如此看重這三種特性,真的那麼重要嗎? 什麼是封裝 什麼是封裝,封裝有什麼好處以及怎麼實現封裝? 相信大多數小夥伴們都有自己的理解,簡而言之,言而簡之,封裝是遮蔽內部實現細節,僅僅對外暴露出有價值介面. 正如平時工作中使用的電

go 學習筆記萬萬沒想到寵物店竟然催生出面向介面程式設計?

到底是要貓還是要狗 在上篇文章中,我們編撰了一則簡短的小故事用於講解了什麼是面向物件的繼承特性以及 Go 語言是如何實現這種繼承語

go 學習筆記無心插柳柳成蔭的介面和無為而治的空介面

如果你還了解程式設計概念中的介面概念,那麼我建議你最好還是先閱讀上一篇文章.詳情請點選 go 學習筆記之萬萬沒想到寵物店竟然催生出面向介面程式設計? ,否則的話,請自動忽略上文,繼續探索 Go 語言的介面有什麼不同之處. 如無法自動跳轉到公眾號「雪之夢技術驛站」文章,可以點選我的頭像,動動你的小手翻翻歷史文

go 學習筆記僅僅需要一個示例就能講清楚什麼

本篇文章是 Go 語言學習筆記之函數語言程式設計系列文章的第二篇,上一篇介紹了函式基礎,這一篇文章重點介紹函式的重要應用之一: 閉包 空談誤國,實幹興邦,以具體程式碼示例為基礎講解什麼是閉包以及為什麼需要閉包等問題,下面我們沿用上篇文章的示例程式碼開始本文的學習吧! 斐波那契數列是形如 1 1 2 3 5

go 學習筆記解讀什麼是defer延遲函式

Go 語言中有個 defer 關鍵字,常用於實現延遲函式來保證關鍵程式碼的最終執行,常言道: "未雨綢繆方可有備無患". 延遲函式就是這麼一種機制,無論程式是正常返回還是異常報錯,只要存在延遲函式都能保證這部分關鍵邏輯最終執行,所以用來做些資源清理等操作再合適不過了. 出入成雙有始有終

javascript成神路:真正理解什麼是

一、閉包是什麼 在函式外部無法訪問函式內部的變數,而函式內可以訪問函式外的變數,在函式的內部,巢狀一個子函式,這個子函式可以訪問父函式作用域內的變數,然後父函式把這個子函式return出去,讓在函式外部可以通過子函式訪問自己的區域性變數,是不是有點懵呢?我們看個例子:

CSS深入理解學習筆記padding

style left 使用 支持 背景色 cnblogs check chrom 所有 1、padding與容器尺寸之間的關系   對於block水平元素:①padding值暴走,一定會影響尺寸;②width非auto,padding影響尺寸;③width為auto或box

CSS深入理解學習筆記relative

需要 定位 層疊 left 避免 str 作用 屬性 存在 1、relative和absolute的相煎關系   限制作用:①限制left/top/right/bottom定位;②限制z-index層級;③限制在overflow下的囂張氣焰。   relative和fixe

CSS深入理解學習筆記line-height

初始 -a 上下 normal 每一個 不同 單行 盒子模型 等於 1、line-height的定義   定義:兩行文字基線之間的距離。   註:不同字體之間的基線是不同的。 2、line-height與行內框盒子模型   行內框盒子模型:   ①內容區域(content