說起golang閉包,在官方手冊里面看過一次,沒怎么用過,還是因為6哥經常用,閱讀他的代碼好多閉包,emmm,今天就學習一下。
在過去近十年時間里,面向對象編程大行其道,以至於在大學的教育里,老師也只會教給我們兩種編程模型,面向過程和面向對象。孰不知,在面向對象思想產生之前,函數式編程已經有了數十年的歷史。就讓我們回顧這個古老又現代的編程模型,看看究竟是什么魔力將這個概念在21世紀的今天再次拉入我們的視野
閉包是函數式編程語言中的概念,沒有研究過函數式語言的人可能很難理解閉包的強大(我就是其中一個,看見的第一眼就是一臉懵逼)
閉包=函數+引用環境
所謂閉包是指內層函數引用了外層函數中的變量或稱為引用了自由變量的函數,其返回值也是一個函數,了解過的語言中有閉包概念的像 js,python,golang
都類似這樣。
閉包只是在形式和表現上像函數,但實際上不是函數。函數是一些可執行的代碼,這些代碼在函數被定義后就確定了,不會在執行時發生變化,所以一個函數只有一個實例。閉包在運行時可以有多個實例,不同的引用環境和相同的函數組合可以產生不同的實例。所謂引用環境是指在程序執行中的某個點所有處於活躍狀態的約束所組成的集合。其中的約束是指一個變量的名字和其所代表的對象之間的聯系。那么為什么要把引用環境與函數組合起來呢?這主要是因為在支持嵌套作用域的語言中,有時不能簡單直接地確定函數的引用環境。這樣的語言一般具有這樣的特性
函數是一等公民(First-class value),即函數可以作為另一個函數的返回值或參數,還可以作為一個變量的值。
函數可以嵌套定義,即在一個函數內部可以定義另一個函數。
在面向對象編程中,我們把對象傳來傳去,那在函數式編程中,要做的是把函數傳來傳去,說成術語,把他叫做高階函數。在數學和計算機科學中,高階函數是至少滿足下列一個條件的函數:
接受一個或多個函數作為輸入
輸出一個函數
上代碼,最基礎的閉包方式
package main import ( "fmt" ) func outer(x int) func(int) int { return func(y int) int { return x + y } } func main() { f := outer(10) fmt.Println(f(100)) }
單純看return x+y
就知道返回結果是什么。也就是110.
上面的例子還不夠簡單,再來一個
func f(i int) func() int { return func() int { i++ return i } }
函數f返回了一個函數,返回的這個函數就是一個閉包。這個函數本身中沒有定義變量I的,而是引用了它所在的環境(函數f)中的變量i。
package main import "fmt" func f(i int) func() int { return func() int { i++ return i } } func main() { a := f(0) fmt.Println(a()) fmt.Println(a()) fmt.Println(a()) fmt.Println(a()) fmt.Println(a()) } // PS C:\Users\13584\go\src\awesomeProject> go run .\main.go // 1 // 2 // 3 // 4 // 5
函數f每進入一次,就形成了一個新的環境,對應的閉包中,函數都是同一個函數,環境卻是引用不同的環境。
變量i是函數f中的局部變量,假設這個變量是在函數f的棧中分配的,是不可以的。因為函數f返回以后,對應的棧就失效了,f返回的那個函數中變量i就引用一個失效的位置了。所以閉包的環境中引用的變量不能夠在棧上分配。
閉包結構體
回到閉包的實現來,前面說過,閉包是函數和它所引用的環境。那么是不是可以表示為一個結構體呢:
type Closure struct { F func()() i *int }
事實上,Go在底層確實就是這樣表示一個閉包的。讓我們看一下匯編代碼:
func f(i int) func() int { return func() int { i++ return i } } MOVQ $type.int+0(SB),(SP) PCDATA $0,$16 PCDATA $1,$0 CALL ,runtime.new(SB) // 是不是很熟悉,這一段就是i = new(int) ... MOVQ $type.struct { F uintptr; A0 *int }+0(SB),(SP) // 這個結構體就是閉包的類型 ... CALL ,runtime.new(SB) // 接下來相當於 new(Closure) PCDATA $0,$-1 MOVQ 8(SP),AX NOP , MOVQ $"".func·001+0(SB),BP MOVQ BP,(AX) // 函數地址賦值給Closure的F部分 NOP , MOVQ "".&i+16(SP),BP // 將堆中new的變量i的地址賦值給Closure的值部分 MOVQ BP,8(AX) MOVQ AX,"".~r1+40(FP) ADDQ $24,SP RET ,
其中func·001是另一個函數的函數地址,也就是f返回的那個函數。
閉包小結:
函數只是一段可執行代碼,編譯后就“固化”了,每個函數在內存中只有一份實例,得到函數的入口點便可以執行函數了。在函數式編程語言中,函數是一等公民(First class value):第一類對象,我們不需要像命令式語言中那樣借助函數指針,委托操作函數,函數可以作為另一個函數的參數或返回值,可以賦給一個變量。函數可以嵌套定義,即在一個函數內部可以定義另一個函數,有了嵌套函數這種結構,便會產生閉包問題。如:
package main import ( "fmt" ) func adder() func(int) int { sum := 0 innerfunc := func(x int) int { sum += x return sum } return innerfunc } func main() { pos, neg := adder(), adder() for i := 0; i < 10; i++ { fmt.Println(pos(i), neg(-2*i)) } }
在這段程序中,函數innerfunc是函數adder的內嵌函數,並且是adder函數的返回值。我們注意到一個問題:內嵌函數innerfunc中引用到外層函數中的局部變量sum,Go會這么處理這個問題呢?先讓我們來看看這段代碼的運行結果:
0 0 1 -2 3 -6 6 -12 10 -20 15 -30 21 -42 28 -56 36 -72 45 -90
注意:Go不能在函數內部顯式嵌套定義函數,但是可以定義一個匿名函數。如上面所示,我們定義了一個匿名函數對象,然后將其賦值給innerfunc,最后將其作為返回值返回。
當用不同的參數調用adder函數得到(pos(i),neg(i))函數時,得到的結果是隔離的,也就是說每次調用adder返回的函數都將生成並保存一個新的局部變量sum。其實這里adder函數返回的就是閉包。
這個就是Go中的閉包,一個函數和與其相關的引用環境組合而成的實體。一句關於閉包的名言: 對象是附有行為的數據,而閉包是附有數據的行為。
三 閉包使用
閉包經常用於回調函數,當IO操作(例如從網絡獲取數據、文件讀寫)完成的時候,會對獲取的數據進行某些操作,這些操作可以交給函數對象處理。
除此之外,在一些公共的操作中經常會包含一些差異性的特殊操作,而這些差異性的操作可以用函數來進行封裝。看下面的例子:
package main import ( "errors" "fmt" ) type Traveser func(ele interface{}) /* Process:封裝公共切片數組操作 */ func Process(array interface{}, traveser Traveser) error { if array == nil { return errors.New("nil pointer") } var length int //數組的長度 switch array.(type) { case []int: length = len(array.([]int)) case []string: length = len(array.([]string)) case []float32: length = len(array.([]float32)) default: return errors.New("error type") } if length == 0 { return errors.New("len is zero.") } traveser(array) return nil } /* 具體操作:升序排序數組元素 */ func SortByAscending(ele interface{}) { intSlice, ok := ele.([]int) if !ok { return } length := len(intSlice) for i := 0; i < length-1; i++ { isChange := false for j := 0; j < length-1-i; j++ { if intSlice[j] > intSlice[j+1] { isChange = true intSlice[j], intSlice[j+1] = intSlice[j+1], intSlice[j] } } if isChange == false { return } } } /* 具體操作:降序排序數組元素 */ func SortByDescending(ele interface{}) { intSlice, ok := ele.([]int) if !ok { return } length := len(intSlice) for i := 0; i < length-1; i++ { isChange := false for j := 0; j < length-1-i; j++ { if intSlice[j] < intSlice[j+1] { isChange = true intSlice[j], intSlice[j+1] = intSlice[j+1], intSlice[j] } } if isChange == false { return } } } func main() { intSlice := make([]int, 0) intSlice = append(intSlice, 3, 1, 4, 2) Process(intSlice, SortByDescending) fmt.Println(intSlice) //[4 3 2 1] Process(intSlice, SortByAscending) fmt.Println(intSlice) //[1 2 3 4] stringSlice := make([]string, 0) stringSlice = append(stringSlice, "hello", "world", "china") /* 具體操作:使用匿名函數封裝輸出操作 */ Process(stringSlice, func(elem interface{}) { if slice, ok := elem.([]string); ok { for index, value := range slice { fmt.Println("index:", index, " value:", value) } } }) floatSlice := make([]float32, 0) floatSlice = append(floatSlice, 1.2, 3.4, 2.4) /* 具體操作:使用匿名函數封裝自定義操作 */ Process(floatSlice, func(elem interface{}) { if slice, ok := elem.([]float32); ok { for index, value := range slice { slice[index] = value * 2 } } }) fmt.Println(floatSlice) //[2.4 6.8 4.8] }
輸出結果
[4 3 2 1] [1 2 3 4] index: 0 value: hello index: 1 value: world index: 2 value: china [2.4 6.8 4.8]
在上面的例子中,Process函數負責對切片(數組)數據進行操作,在操作切片(數組)時候,首先要做一些參數檢測,例如指針是否為空、數組長度是否大於0等。這些是操作數據的公共操作。具體針對數據可以有自己特殊的操作,包括排序(升序、降序)、輸出等。針對這些特殊的操作可以使用函數對象來進行封裝。
再看下面的例子,這個例子沒什么實際意義,只是為了說明閉包的使用方式。
package main import ( "fmt" ) type FilterFunc func(ele interface{}) interface{} /* 公共操作:對數據進行特殊操作 */ func Data(arr interface{}, filterFunc FilterFunc) interface{} { slice := make([]int, 0) array, _ := arr.([]int) for _, value := range array { integer, ok := filterFunc(value).(int) if ok { slice = append(slice, integer) } } return slice } /* 具體操作:奇數變偶數(這里可以不使用接口類型,直接使用int類型) */ func EvenFilter(ele interface{}) interface{} { integer, ok := ele.(int) if ok { if integer%2 == 1 { integer = integer + 1 } } return integer } /* 具體操作:偶數變奇數(這里可以不使用接口類型,直接使用int類型) */ func OddFilter(ele interface{}) interface{} { integer, ok := ele.(int) if ok { if integer%2 != 1 { integer = integer + 1 } } return integer } func main() { sliceEven := make([]int, 0) sliceEven = append(sliceEven, 1, 2, 3, 4, 5) sliceEven = Data(sliceEven, EvenFilter).([]int) fmt.Println(sliceEven) //[2 2 4 4 6] sliceOdd := make([]int, 0) sliceOdd = append(sliceOdd, 1, 2, 3, 4, 5) sliceOdd = Data(sliceOdd, OddFilter).([]int) fmt.Println(sliceOdd) //[1 3 3 5 5] }
輸出結果
[2 2 4 4 6] [1 3 3 5 5]
四 總結
上面例子中閉包的使用有點類似於面向對象設計模式中的模版模式,在模版模式中是在父類中定義公共的行為執行序列,然后子類通過重載父類的方法來實現特定的操作,而在Go語言中我們使用閉包實現了同樣的效果。
其實理解閉包最方便的方法就是將閉包函數看成一個類,一個閉包函數調用就是實例化一個類(在Objective-c中閉包就是用類來實現的),然后就可以從類的角度看出哪些是“全局變量”,哪些是“局部變量”。例如在第一個例子中,pos和neg分別實例化了兩個“閉包類”,在這個“閉包類”中有個“閉包全局變量”sum。所以這樣就很好理解返回的結果了。