前言
入坑 Go
語言已經大半年了,卻沒有寫過一篇像樣的技術文章,每次寫一半就擱筆,然後就爛尾了。
幾經思考,痛定思痛,決定金盆洗手,重新做人,哦不,重新開始寫技術博文。
這段時間在研究Go語言閉包
的過程中,發現了很多有意思的東西,也學到了不少內容,於是便以次為契機,重新開始技術文章的輸出。
什麼是閉包
閉包
是 Go
語言中一個重要特性,也是 函數語言程式設計
中必不可少的角色。那麼什麼是 閉包
呢?
A closure is a function value that references variables from outside its body.
這是 A Tour of Go
上的定義,閉包
是一種引用了外部變數的函式。但我覺得這個定義還不夠準確,閉包
應該是引用了外部變數的 匿名函式
。
看了很多文章,大多把 閉包
跟匿名函式
混淆在了一起,也有很多人說,閉包
其實就是匿名函式
,但其實兩者是不能直接劃等號的。
閉包
是一種特殊的匿名函式
,是匿名函式
的子集。所以在說 閉包
之前,我們先來看看 匿名函式
吧。
匿名函式
匿名函式
顧名思義,就是沒有名字的函式。在Go語言中,函式是一等公民,也就是說,函式可以被賦值或者當作返回值和引數進行傳遞,在很多時候我們並不需要一個有名字的函式(而且命名確實是一項相當費勁的事),所以我們在某些場景下可以選擇使用 匿名函式
。
舉個例子:
func main(){
hello := func(){
fmt.Println("Hello World")
}
hello()
}
這是一個簡單的例子,我們宣告瞭一個 匿名函式
,然後把它賦值給一個叫 hello
的變數,然後我們就能像呼叫函式那樣使用它了。
這跟下面的程式碼效果是一樣的:
func main(){
hello()
}
func hello(){
fmt.Println("Hello World")
}
我們還可以把 匿名函式
當作函式引數進行傳遞:
func main(){
doPrint("Hello World", func(s string){
fmt.Println(s)
})
}
type Printer func(string)
func doPrint(s string, printer Printer){
printer(s)
}
或者當作函式返回值進行返回:
func main(){
getPrinter()("Hello World")
}
type Printer func(string)
func getPrinter()Printer{
return func(s string){
fmt.Println(s)
}
}
匿名函式
跟普通函式在絕大多數場景下沒什麼區別,普通函式的函式名可以當作是與該函式繫結的函式常量。
一個函式主要包含兩個資訊:函式簽名和函式體,函式的簽名包括引數型別,返回值的型別,函式簽名可以看做是函式的型別,函式的函式體即函式的值。所以一個接收匿名函式的變數的型別便是由函式的簽名決定的,一個匿名函式被賦值給一個變數後,這個變數便只能接收同樣簽名的函式。
func main(){
hello := func(){
fmt.Println("Hello World")
} // 給 hello 變數賦值一個匿名函式
hello()
hello = func(){
fmt.Println("Hello World2")
} // 重新賦值新的匿名函式
hello()
hello = hi // 將一個普通函式賦值給 hello
hello()
hello = func(int){
fmt.Println("Hello World3")
} // 這裡編譯器會報錯
hello()
}
func hi(){
fmt.Println("Hi")
}
匿名函式
跟普通函式的微小區別在於 匿名函式
賦值的變數可以重新設定新的 匿名函式
,但普通函式的函式名是與特定函式繫結的,無法再將其它函式賦值給它。這就類似於變數與常量之間的區別。
閉包的特性
說完了 匿名函式
,我們再回過頭來看看 閉包
。
閉包
是指由一個擁有許多變數和繫結了這些變數的環境的匿名函式
閉包
=函式
+引用環境
聽起來有點繞,什麼是 引用環境
呢?
引用環境
是指在程式執行中的某個點所有處於活躍狀態的變數所組成的集合。
由於閉包
把函式和執行時的引用環境打包成為一個新的整體,所以就解決了函式程式設計中的巢狀所引發的問題。
當每次呼叫包含閉包
的函式時都將返回一個新的閉包例項,這些例項之間是隔離的,分別包含呼叫時不同的引用環境現場。不同於函式,閉包
在執行時可以有多個例項,不同的引用環境和相同的函式組合可以產生不同的例項。
簡單來說,閉包
就是引用了外部變數的匿名函式。不太明白?沒關係,讓我們先來看一個栗子:
func adder() func() int {
var i = 0
return func() int {
i++
return i
}
}
這是用閉包
實現的簡單累加器,這一部分便是閉包,它引用在其作用域範圍之外的變數i。
func() int {
i++
return i
}
可以這樣使用:
func main() {
a := adder()
fmt.Println(a())
fmt.Println(a())
fmt.Println(a())
fmt.Println(a())
b := adder()
fmt.Println(b())
fmt.Println(b())
}
輸出如下:
1
2
3
4
1
2
上述例子中,adder
是一個函式,沒有入參,返回值是一個返回 int
型別的無參函式,也就是說呼叫 adder
函式會返回一個函式,這個函式的返回值是 int
型別,且不接收引數。
在 main
方法中:
a := adder()
這裡是將呼叫後得到的函式賦值給了變數 a
,隨後進行了四次函式呼叫和輸出:
fmt.Println(a())
fmt.Println(a())
fmt.Println(a())
fmt.Println(a())
也許你還是會感到困惑,i
是 adder
函式裡的變數,呼叫完成之後變數的生命週期不久結束了嗎?為什麼還能不斷累加?
這就涉及到閉包
的另一個重要話題了:閉包
會讓被引用的區域性變數從棧逃逸到堆上,從而使其能在其作用域範圍之外存活。閉包
“捕獲”了和它在同一作用域的其它常量和變數。這就意味著當閉包
被呼叫的時候,不管在程式什麼地方呼叫,閉包
能夠使用這些常量或者變數。它不關心這些捕獲了的變數和常量是否已經超出了作用域,只要閉包
還在使用它們,這些變數就還會存在。
匿名函式和閉包的使用
可以利用匿名函式
和閉包
可以實現很多有意思的功能,比如上面的累加器,便是利用了 閉包
的作用域隔離特性,每呼叫一次 adder
函式,就會生成一個新的累加器,使用新的變數 i
,所以在呼叫 b()
時,仍舊會從1開始輸出。
再來看幾個匿名函式
和閉包
應用的例子。
工廠函式
工廠函式即生產函式的函式,呼叫工廠函式可以得到其內嵌函式的引用,每次呼叫都可以得到一個新的函式引用。
func getFibGen() func() int {
f1 := 0
f2 := 1
return func() int {
f2, f1 = f1 + f2, f2
return f1
}
}
func main() {
gen := getFibGen()
for i := 0; i < 10; i++ {
fmt.Println(gen())
}
}
上面是利用閉包實現的函式工廠來求解斐波那契數列問題,呼叫 getFibGen
函式之後,gen
便獲得了內嵌函式的引用,且該函式引用裡一直持有 f1
和 f2
的引用,每執行一次 gen()
,便會運算一次斐波那契的遞推關係式:
func() int {
f2, f1 = f1 + f2, f2
return f1
}
輸出如下:
1
1
2
3
5
8
13
21
34
55
由於閉包
能構造出單獨的變數環境,可以很好的實現環境隔離,所以很適合應用於函式工廠,在實現功能時儲存某些狀態變數。
裝飾器/中介軟體
修飾器
是指在不改變物件的內部結構情況下,動態地擴充套件物件的功能。通過建立一個裝飾器,來包裝真實的物件。使用閉包
很容易實現裝飾器模式
。
在 gin 中的 Middleware 便是使用裝飾器模式
來實現的。比如我們可以這樣實現一個自定義的 Logger:
func Logger() gin.HandlerFunc {
return func(context *gin.Context) {
host := context.Request.Host
url := context.Request.URL
method := context.Request.Method
fmt.Printf("%s::%s \t %s \t %s \n", time.Now().Format("2006-01-02 15:04:05"), host, url, method)
context.Next()
fmt.Println("response status: ", context.Writer.Status())
}
}
這是在 gin
中利用 匿名函式
實現的自定義日誌中介軟體,在 gin
中,類似的用法十分常見。
defer
這是匿名函式
和閉包
最常用的地方,我們會經常在 defer
函式中使用匿名函式
和閉包
來做釋放鎖,關閉連線,處理 panic
等函式善後工作。
func main() {
defer func() {
if ok := recover(); ok != nil {
fmt.Println("recover from panic")
}
}()
panic("error")
}
gorutine
匿名函式
和閉包
還有一個十分常用的場景,那便是在啟動 gorutine
時使用。
func main(){
go func(){
fmt.Println("Hello World")
}()
time.Sleep(1 * time.Second)
}
重新宣告一下,在函式內部引用了外部變數便是閉包
,否則就是匿名函式
。
func main(){
hello := "Hello World"
go func(){
fmt.Println(hello)
}()
time.Sleep(1 * time.Second)
}
context
在cancelContext中也使用到了閉包:
// A CancelFunc tells an operation to abandon its work.
// A CancelFunc does not wait for the work to stop.
// A CancelFunc may be called by multiple goroutines simultaneously.
// After the first call, subsequent calls to a CancelFunc do nothing.
type CancelFunc func()
// WithCancel returns a copy of parent with a new Done channel. The returned
// context's Done channel is closed when the returned cancel function is called
// or when the parent context's Done channel is closed, whichever happens first.
//
// Canceling this context releases resources associated with it, so code should
// call cancel as soon as the operations running in this Context complete.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc) {
c := newCancelCtx(parent)
propagateCancel(parent, &c)
return &c, func() { c.cancel(true, Canceled) }
}
// newCancelCtx returns an initialized cancelCtx.
func newCancelCtx(parent Context) cancelCtx {
return cancelCtx{Context: parent}
}
閉包的陷阱
閉包很好用,但在某些場景下,也十分具有欺騙性,稍有不慎,就會掉入其陷阱裡。
不如先來看一個例子:
for j := 0; j < 2; j++ {
defer func() {
fmt.Println(j)
}()
}
你猜會輸出什麼?
2
2
這是因為在 defer
中使用的閉包引用了外部變數 j
。
閉包
中持有的是外部變數的引用
這是很容易犯的錯誤,在迴圈體中使用 defer
,來關閉連線,釋放資源,但由於閉包內持有的是外部變數的引用,在這裡持有的是變數 j
的引用,defer
會在函式執行完成前呼叫閉包,在開始執行閉包時,j
的值已經是2了。
那麼這個問題應該如何修復呢?有兩種方式,一種是重新定義變數:
for j := 0; j < 2; j++ {
k := j
defer func() {
fmt.Println(k)
}()
}
在迴圈體裡,每次迴圈都定義了一個新的變數 k
來獲取原變數 j
的值,因此每次呼叫閉包
時,引用的是不同的變數 k
,從而達到變數隔離的效果。
另一種方式是把變數當成引數傳入:
for j := 0; j < 2; j++ {
defer func(k int) {
fmt.Println(k)
}(j)
}
這裡每次呼叫閉包
時,傳入的都是變數 j
的值,雖然 defer
仍會在函式執行完成前呼叫,但傳入閉包
的引數值卻是先計算好的,因而能夠正確輸出。
閉包
返回的包裝物件是一個複合結構,裡面包含匿名函式
的地址,以及環境變數
的地址。
為了更好的理解這一點,我們再來看一個例子:
package main
import "fmt"
func main() {
x, y := 1, 2
defer func(a int) {
fmt.Printf("x:%d,y:%d\n", a, y)
}(x)
x += 1
y += 1
fmt.Println(x, y)
}
輸出如下:
2 3
x:1,y:3
另外,由於閉包
會使得其持有的外部變數逃逸出原有的作用域,所以使用不當可能會造成記憶體洩漏,這一點由於相當具有隱蔽性,所以也需要謹慎對待。
總結
閉包
是一種特殊的匿名函式
,是由函式體和引用的外部變數一起組成,可以看成類似如下結構:
type FF struct {
F unitptr
A *int
B *int
X *int // 如果X是string/[]int,那麼這裡應該為*string,*[]int
}
在Go語言中,閉包的應用十分廣泛,掌握了閉包的使用可以讓你在寫程式碼時能更加遊刃有餘,也可以避免很多不必要的麻煩。所以是必須要掌握的一個知識點。
至此,關於閉包的內容就完結了,希望能對你有幫助。