Go中的閉包函數(shù)Closure示例詳解
前言
“閉包”(Closure)是編程中一個非常重要、但初學(xué)者容易暈的概念。它在函數(shù)式編程(Functional Programming)中無處不在,Go 語言對它的支持非常強大。
簡單來說,閉包就是:一個函數(shù)和它周圍環(huán)境的綁定。
為了讓你徹底理解,我們從三個層面來拆解:通俗定義、代碼演示、以及底層原理。
1. 通俗定義:自帶“背包”的函數(shù)
通常情況下,一個函數(shù)執(zhí)行完,它內(nèi)部定義的變量就會被銷毀(從內(nèi)存棧中彈出)。
但是閉包不一樣。閉包是一個特殊的函數(shù),它被創(chuàng)建的時候,會偷偷把當(dāng)時它能看到的變量抓取過來,裝進自己的“背包”里帶走。
公式:
閉包 = 函數(shù)代碼 + 捕獲的外部變量
哪怕創(chuàng)造它的那個外部函數(shù)已經(jīng)執(zhí)行結(jié)束了,閉包依然能通過“背包”訪問和修改那些變量。
2. 代碼演示:最經(jīng)典的計數(shù)器
這是理解閉包的“Hello World”例子。
Go
package main
import "fmt"
// seq 此時是一個“工廠”,它返回一個函數(shù)
// 這個返回的函數(shù)返回 int
func seq() func() int {
i := 0 // 這是一個局部變量,按理說 seq 執(zhí)行完它就該銷毀了
// 返回一個匿名函數(shù)
return func() int {
i++ // 這個匿名函數(shù)引用了外部的 i
return i
}
}
func main() {
// 1. 創(chuàng)建第一個閉包實例 nextNum
// 此時 seq() 執(zhí)行結(jié)束,但變量 i 被 nextNum 的背包抓走了
nextNum := seq()
fmt.Println(nextNum()) // 輸出 1
fmt.Println(nextNum()) // 輸出 2
fmt.Println(nextNum()) // 輸出 3 (i 的狀態(tài)被保留了!)
// 2. 創(chuàng)建第二個閉包實例 nextNum2
// 它會有自己全新的 i,和上面那個互不干擾
nextNum2 := seq()
fmt.Println(nextNum2()) // 輸出 1
}
為什么叫閉包? 注意看 i。它定義在 seq 里,但在 main 里通過 nextNum() 依然能一直修改它。nextNum 這個函數(shù)把 i “封閉”在自己的作用域里了,這就叫閉包。
3. 底層原理:變量去哪了?(CS 專業(yè)向)
作為一個計算機專業(yè)的學(xué)生,你可能會問:“棧上的局部變量不是函數(shù)返回就銷毀了嗎?為什么 i 還能活下來?”
這里涉及到了 Go 編譯器的逃逸分析 (Escape Analysis)。
正常情況:函數(shù)內(nèi)部的變量分配在棧 (Stack) 上,函數(shù)結(jié)束,棧幀彈出,數(shù)據(jù)銷毀。
閉包情況:編譯器掃描代碼,發(fā)現(xiàn)變量
i被內(nèi)部的匿名函數(shù)引用了,而且這個匿名函數(shù)被返回到了外部。逃逸:編譯器判定
i不能死在棧上,于是把它分配到了堆 (Heap) 上。引用:返回的閉包函數(shù)(
nextNum)內(nèi)部持有一個指針,指向堆上的這個i。
所以,當(dāng)你調(diào)用 makeHandler(viewHandler) 時,那層“中間件”之所以能記住 viewHandler 是誰,就是因為 fn 這個變量逃逸到了堆上,被返回的那個匿名函數(shù)死死抓在手里。
4. 閉包在實戰(zhàn)中的三大用途
回到你的 HTTP 題目,以及以后的開發(fā)中,閉包主要有這就三種用法:
A. 封裝狀態(tài)(像簡化的類)
不需要專門寫一個 struct 或者 class 來存狀態(tài),直接用閉包。上面的計數(shù)器就是例子。
B. 延遲執(zhí)行 / 回調(diào) (Callback)
當(dāng)你把一個函數(shù) A 傳給另一個函數(shù) B,B 在未來某個時間點調(diào)用 A,A 還能記得當(dāng)初傳給它的數(shù)據(jù)。
C. 中間件 / 裝飾器 (Middleware / Decorator)
這就是你那段代碼的本質(zhì)。 你想給原本的函數(shù) viewHandler 加一層“安檢”。
輸入:一個舊函數(shù)。
輸出:一個新函數(shù)(新函數(shù) = 安檢邏輯 + 舊函數(shù))。
Go
// 這就是一個典型的裝飾器模式(Decorator Pattern)
func makeHandler(fn ...) http.HandlerFunc {
return func(...) {
// 1. 前置邏輯 (Pre-processing)
// ... 驗證 title ...
// 2. 調(diào)用原函數(shù) (Original logic)
fn(...)
// 3. 后置邏輯 (Post-processing) - 可選
}
}
總結(jié)
閉包就是能記住它出生環(huán)境的函數(shù)。
在 Go 里,閉包讓局部變量逃逸到堆上,從而延長了變量的生命周期。
在很多場景下(比如你的 Web 路由處理),用閉包比定義一個 Interface 或者 Struct 更輕量、更優(yōu)雅。
看一段代碼:
package main
import "fmt"
func adder() func(int) int {
sum := 0
return func(x int) int {
sum += x
return sum
}
}
func main() {
pos, neg := adder(), adder()
for i := 0; i < 10; i++ {
fmt.Println(
pos(i),
neg(-2*i),
)
}
}
輸出結(jié)果:
0 0
1 -2
3 -6
6 -12
10 -20
15 -30
每次調(diào)用 makeHandler(工廠),都會產(chǎn)生一個新的、獨立的閉包環(huán)境。它們互不干擾,不引用同一個變量。
為了讓你徹底明白,我們要分兩種情況來看:
情況一:調(diào)用兩次“工廠函數(shù)” (你的 main 函數(shù)里的情況)
這是你在 main 函數(shù)里寫的代碼邏輯:
Go
// 第 1 次調(diào)用 makeHandler // 產(chǎn)生了一個閉包 A,它的背包里裝的是 viewHandler h1 := makeHandler(viewHandler) // 第 2 次調(diào)用 makeHandler // 產(chǎn)生了一個閉包 B,它的背包里裝的是 editHandler h2 := makeHandler(editHandler)
結(jié)論: 它們不引用同一個變量。
原理: 每次你調(diào)用 makeHandler 時,Go 都會在內(nèi)存中開辟一個新的棧幀 (Stack Frame)。
第一次調(diào)用時,參數(shù)
fn是viewHandler。這個fn也就是閉包 A “背包”里的東西。第二次調(diào)用時,參數(shù)
fn是editHandler。這個fn是閉包 B “背包”里的東西。
雖然變量名都叫 fn,但它們在內(nèi)存里是兩塊完全不同的地址。就好比你去了兩次肯德基,第一次買了漢堡,第二次買了薯條。雖然都裝在“打包袋”里,但這是兩個不同的袋子,裝的東西也不一樣。
情況二:調(diào)用同一個“閉包實例”多次
這是當(dāng)服務(wù)器運行起來,多個用戶訪問同一個 URL 時的情況:
Go
// 假設(shè)這是 main 函數(shù)里注冊好的那個 handler viewHandlerWrapper := makeHandler(viewHandler) // 用戶甲訪問 /view/1 viewHandlerWrapper(w1, r1) // 用戶乙訪問 /view/2 viewHandlerWrapper(w2, r2)
結(jié)論: 它們引用的是同一個外部變量。
原理: 因為 viewHandlerWrapper 是同一個閉包實例。 在這個例子里,它們引用的那個“外部變量”就是被捕獲的 fn (也就是 viewHandler)。 無論用戶調(diào)用多少次 viewHandlerWrapper,它都會去自己的背包里找那個 fn。這也是正確的,因為我們希望大家用的處理邏輯是一樣的。
用那個計數(shù)器的例子看最清楚
回到剛才的計數(shù)器 seq,這能最直觀地展示“變量隔離”:
Go
func seq() func() int {
i := 0 // 這就是那個“外部變量”
return func() int {
i++
return i
}
}
func main() {
// 【工廠調(diào)用兩次】:創(chuàng)造了兩個獨立的世界
counterA := seq()
counterB := seq()
// counterA 的背包里有一個 i (地址 0x1111)
// counterB 的背包里有一個 i (地址 0x2222)
fmt.Println(counterA()) // 輸出 1 (修改的是 0x1111)
fmt.Println(counterA()) // 輸出 2 (修改的是 0x1111)
// 關(guān)鍵點來了:
fmt.Println(counterB()) // 輸出 1 (修改的是 0x2222)
// 發(fā)現(xiàn)了嗎?counterB 從頭開始計數(shù),完全沒受 counterA 影響!
}
總結(jié)你的 makeHandler 代碼
在你的代碼中:
Go
func makeHandler(fn func(...)) http.HandlerFunc {
return func(w, r) {
// ...
fn(w, r, title) // 這里的 fn 是被捕獲的
}
}
makeHandler(viewHandler)創(chuàng)建了一個閉包,它獨占viewHandler。makeHandler(editHandler)創(chuàng)建了另一個閉包,它獨占editHandler。
它們互不干擾,絕對安全。 這也是為什么閉包在 Go 的并發(fā)編程和中間件設(shè)計中如此好用的原因——它天然地實現(xiàn)了狀態(tài)的隔離。
到此這篇關(guān)于Go中閉包函數(shù)Closure的文章就介紹到這了,更多相關(guān)Go閉包函數(shù)Closure內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
在ubuntu下構(gòu)建go語言開發(fā)環(huán)境的方法
這篇文章主要介紹了在ubuntu下構(gòu)建go語言開發(fā)環(huán)境的方法,需要的朋友可以參考下2014-10-10

