Go語言開發(六)、Go語言閉包
一、函數式編程
1、函數式編程簡介
函數式編程是一種編程模型,將電腦運算看作是數學中函數的計算,並且避免了狀態以及變數的概念。
在物件導向思想產生前,函數式編程已經有數十年的曆史。隨著硬體效能的提升以及編譯技術和虛擬機器技術的改進,一些曾被效能問題所限制的動態語言開始受到關注,Python、Ruby和Lua等語言都開始在應用中嶄露頭角。動態語言因其方便快捷的開發方式成為很多人喜愛的程式設計語言,伴隨動態語言的流行,函數式編程也開始流行。
2、函數式編程的特點
函數式編程的主要特點如下:
A、變數的不可變性: 變數一經賦值不可改變。如果需要改變,則必須複製出去,然後修改。
B、函數是一等公民: 函數也是變數,可以作為參數、傳回值等在程式中進行傳遞。
C、尾遞迴:如果遞迴很深的話,堆棧可能會爆掉,並導致效能大幅度下降。而尾遞迴最佳化技術(需要編譯器支援)可以在每次遞迴時重用stack。
3、高階函數
在函數式編程中,函數需要作為參數傳遞,即高階函數。在數學和電腦科學中,高階函數是至少滿足下列一個條件的函數:
A、函數可以作為參數被傳遞
B、函數可以作為傳回值輸出
二、匿名函數
1、匿名函數簡介
匿名函數是指不需要定義函數名的一種函數實現方式,匿名函數由一個不帶函數名的函式宣告和函數體組成。C和C++不支援匿名函數。
func(x,y int) int { return x + y}
2、匿名函數的實值型別
在Go語言中,所有的函數是實值型別,即可以作為參數傳遞,又可以作為傳回值傳遞。
匿名函數可以賦值給一個變數:
f := func() int { ...}
定義一種函數類型:
type CalcFunc func(x, y int) int
函數可以作為值傳遞:
func AddFunc(x, y int) int {return x + y}func SubFunc(x, y int) int { return x - y}...func OperationFunc(x, y int, calcFunc CalcFunc) int { return calcFunc(x, y)}func main() { sum := OperationFunc(1, 2, AddFunc) difference := OperationFunc(1, 2, SubFunc) ...}
函數可以作為傳回值:
// 第一種寫法func add(x, y int) func() int { f := func() int { return x + y } return f}// 第二種寫法func add(x, y int) func() int { return func() int { return x + y }}
當函數返回多個匿名函數時建議採用第一種寫法:
func calc(x, y int) (func(int), func()) { f1 := func(z int) int { return (x + y) * z / 2 } f2 := func() int { return 2 * (x + y) } return f1, f2}
匿名函數的調用有兩種方法:
// 通過傳回值調用func main() { f1, f2 := calc(2, 3) n1 := f1(10) n2 := f1(20) n3 := f2() fmt.Println("n1, n2, n3:", n1, n2, n3)}// 在匿名函數定義的同時進行調用:花括弧後跟參數列表表示函數調用func safeHandler() { defer func() { err := recover() if err != nil { fmt.Println("some exception has happend:", err) } }() ...}
三、閉包
1、閉包的定義
函數可以嵌套定義(嵌套的函數一般為匿名函數),即在一個函數內部可以定義另一個函數。Go語言通過匿名函數支援閉包,C++不支援匿名函數,在C++11中通過Lambda運算式支援閉包。
閉包是由函數及其相關引用環境組合而成的實體(即:閉包=函數+引用環境)。
閉包只是在形式和表現上像函數,但實際上不是函數。函數是一些可執行檔代碼,函數代碼在函數被定義後就確定,不會在執行時發生變化,所以一個函數只有一個執行個體。閉包在運行時可以有多個執行個體,不同的引用環境和相同的函數組合可以產生不同的執行個體。
所謂引用環境是指在程式執行中的某個點所有處於活躍狀態的約束所組成的集合。約束是指一個變數的名字和其所代表的對象之間的聯絡。由於在支援嵌套範圍的語言中,有時不能簡單直接地確定函數的引用環境,因此需要將引用環境與函數組合起來。
2、閉包的本質
閉包是包含自由變數的代碼塊,變數不在代碼塊內或者任何全域上下文中定義,而是在定義代碼塊的環境中定義。由於自由變數包含在代碼塊中,所以只要閉包還被使用,那麼自由變數以及引用的對象就不會被釋放,要執行的代碼為自由變數提供綁定的計算環境。
閉包可以作為函數對象或者匿名函數。支援閉包的多數語言都將函數作為第一級對象,即函數可以儲存到變數中作為參數傳遞給其它函數,能夠被函數動態建立和返回。
func add(n int) func(int) int { sum := n f := func(x int) int { var i int = 2 sum += i * x return sum } return f}
add函數中函數變數為f,自由變數為sum,同時f為sum提供綁定的計算環境,sum和f組成的代碼塊就是閉包。add函數的傳回值是一個閉包,而不僅僅是f函數的地址。在add閉包函數中,只有內部的匿名函數f才能訪問局部變數i,而無法通過其它途徑訪問,因此閉包保證了i的安全性。
當分別用不同的參數(10, 20)注入add函數而得到不同的閉包函數變數時,得到的結果是隔離的,即每次調用add函數後都將產生並儲存一個新的局部變數sum。
在函數式語言中,當內嵌函數體內引用到體外的變數時,將會把定義時涉及到的引用環境和函數體打包成一個整體(閉包)返回。
當每次調用add函數時都將返回一個新的閉包執行個體,不同執行個體之間是隔離的,分別包含調用時不同的引用環境現場。不同於函數,閉包在運行時可以有多個執行個體,不同的引用環境和相同的函數組合可以產生不同的執行個體。
從形式上看,匿名函數都是閉包。
函數只是一段可執行代碼,編譯後就固定,每個函數在記憶體中只有一份執行個體,得到函數的進入點便可以執行函數。
對象是附有行為的資料,而閉包是附有資料的行為。
3、閉包的使用
閉包經常用於回呼函數,當IO操作(例如從網路擷取資料、檔案讀寫)完成的時候,會對擷取的資料進行某些操作,操作可以交給函數對象處理。
除此之外,在一些公用的操作中經常會包含一些差異性的特殊操作,而差異性的操作可以用函數來進行封裝。
package mainimport "fmt"func adder() func(int) int { sum := 0 f := func(x int) int { sum += x return sum } return f}func main() { sum := adder() for i := 0; i < 10; i++ { fmt.Println(sum(i)) }}
四、閉包的應用
package mainimport "fmt"//普通閉包func adder() func(int) int { sum := 0 return func(v int) int { sum += v return sum }}//無狀態、無變數的閉包type iAdder func(int) (int, iAdder)func adder2(base int) iAdder { return func(v int) (int, iAdder) { return base + v, adder2(base + v) }}//使用閉包實現斐波那契數列func Fibonacci() func() int { a, b := 0, 1 return func() int { a, b = b, a+b return a }}func main() { //普通閉包調用 a := adder() for i := 0; i < 10; i++ { var s int =a(i) fmt.Printf("0 +...+ %d = %d\n",i, s) } //狀態 無變數的閉包 調用 b := adder2(0) for i := 0; i < 10; i++ { var s int s, b = b(i) fmt.Printf("0 +...+ %d = %d\n",i, s) } //調用斐波那契數列產生 fib:=Fibonacci() fmt.Println(fib(),fib(),fib(),fib(),fib(),fib(),fib(),fib())}