前言
入坑 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語言中,閉包的應用十分廣泛,掌握了閉包的使用可以讓你在寫代碼時能更加游刃有余,也可以避免很多不必要的麻煩。所以是必須要掌握的一個知識點。
至此,關於閉包的內容就完結了,希望能對你有幫助。