一文徹底理解Golang閉包實(shí)現(xiàn)原理
前言
閉包對(duì)于一個(gè)長(zhǎng)期寫 Java
的開發(fā)者來(lái)說估計(jì)鮮有耳聞,我在寫 Python
和 Go
之前也是沒怎么了解,光這名字感覺就有點(diǎn)"神秘莫測(cè)",這篇文章的主要目的就是從編譯器的角度來(lái)分析閉包,徹底搞懂閉包的實(shí)現(xiàn)原理。
函數(shù)一等公民
一門語(yǔ)言在實(shí)現(xiàn)閉包之前首先要具有的特性就是:First class function
函數(shù)是第一公民。
簡(jiǎn)單來(lái)說就是函數(shù)可以像一個(gè)普通的值一樣在函數(shù)中傳遞,也能對(duì)變量賦值。
先來(lái)看看在 Go 里是如何編寫的:
package main import "fmt" var varExternal int func f1() func(int) int { varInner := 20 innerFun := func(a int) int { fmt.Println(a) varExternal++ varInner++ return varInner } return innerFun } func main() { varExternal = 10 f2 := f1() for i := 0; i < 2; i++ { fmt.Printf("varInner=%d, varExternal=%d \n", f2(i), varExternal) } fmt.Println("======") f3 := f1() for i := 0; i < 2; i++ { fmt.Printf("varInner=%d, varExternal=%d \n", f3(i), varExternal) } } // Output: 0 varInner=21, varExternal=11 1 varInner=22, varExternal=12 ====== 0 varInner=21, varExternal=13 1 varInner=22, varExternal=14
這里體現(xiàn)了閉包的兩個(gè)重要特性,第一個(gè)自然就是函數(shù)可以作為值返回,同時(shí)也能賦值給變量。
第二個(gè)就是在閉包函數(shù) f1()
對(duì)閉包變量 varInner
的訪問,每個(gè)閉包函數(shù)的引用都會(huì)在自己的函數(shù)內(nèi)部保存一份閉包變量 varInner
,這樣在調(diào)用過程中就不會(huì)互相影響。
從打印的結(jié)果中也能看出這個(gè)特性。
作用域
閉包之所以不太好理解的主要原因是它不太符合自覺。
本質(zhì)上就是作用域的關(guān)系,當(dāng)我們調(diào)用 f1()
函數(shù)的時(shí)候,會(huì)在棧中分配變量 varInner
,正常情況下調(diào)用完畢后 f1
的棧會(huì)彈出,里面的變量 varInner
自然也會(huì)銷毀才對(duì)。
但在后續(xù)的 f2()
和 f3()
調(diào)用的時(shí),卻依然能訪問到 varInner
,就這點(diǎn)不符合我們對(duì)函數(shù)調(diào)用的直覺。
但其實(shí)換個(gè)角度來(lái)看,對(duì) innerFun
來(lái)說,他能訪問到 varExternal
和 varInner
變量,最外層的 varExternal
就不用說了,一定是可以訪問的。
但對(duì)于 varInner
來(lái)說就不一定了,這里得分為兩種情況;重點(diǎn)得看該語(yǔ)言是靜態(tài)/動(dòng)態(tài)作用域。
就靜態(tài)作用域來(lái)說,每個(gè)符號(hào)在編譯器就確定好了樹狀關(guān)系,運(yùn)行時(shí)不會(huì)發(fā)生變化;也就是說 varInner
對(duì)于 innerFun
這個(gè)函數(shù)來(lái)說在編譯期已經(jīng)確定可以訪問了,在運(yùn)行時(shí)自然也是可以訪問的。
但對(duì)于動(dòng)態(tài)作用域來(lái)說,完全是在運(yùn)行時(shí)才確定訪問的變量是哪一個(gè)。
恰好 Go
就是一個(gè)靜態(tài)作用域的語(yǔ)言,所以返回的 innerFun
函數(shù)可以一直訪問到 varInner
變量。
實(shí)現(xiàn)閉包
但 Go 是如何做到在 f1()
函數(shù)退出之后依然能訪問到 f1()
中的變量呢?
這里我們不妨大膽假設(shè)一下:
首先在編譯期掃描出哪些是閉包變量,也就是這里的 varInner
,需要將他保存到函數(shù) innerFun()
中。
f2 := f1() f2()
運(yùn)行時(shí)需要判斷出 f2
是一個(gè)函數(shù),而不是一個(gè)變量,同時(shí)得知道它所包含的函數(shù)體是 innerFun()
所定義的。
接著便是執(zhí)行函數(shù)體的 statement
即可。
而當(dāng) f3 := f1()
重新賦值給 f3
時(shí),在 f2
中累加的 varInner
變量將不會(huì)影響到 f3
,這就得需要在給 f3
賦值的重新賦值一份閉包變量到 f3
中,這樣便能達(dá)到互不影響的效果。
閉包掃描
GScript
本身也是支持閉包的,所以把 Go 的代碼翻譯過來(lái)便長(zhǎng)這樣:
int varExternal =10; func int(int) f1(){ int varInner = 20; int innerFun(int a){ println(a); int c=100; varExternal++; varInner++; return varInner; } return innerFun; } func int(int) f2 = f1(); for(int i=0;i<2;i++){ println("varInner=" + f2(i) + ", varExternal=" + varExternal); } println("======="); func int(int) f3 = f1(); for(int i=0;i<2;i++){ println("varInner=" + f3(i) + ", varExternal=" + varExternal); } // Output: 0 varInner=21, varExternal=11 1 varInner=22, varExternal=12 ======= 0 varInner=21, varExternal=13 1 varInner=22, varExternal=14
可以看到運(yùn)行結(jié)果和 Go 的一樣,所以我們來(lái)看看 GScript
是如何實(shí)現(xiàn)的便也能理解 Go 的原理了。
先來(lái)看看第一步掃描閉包變量:
allVariable := c.allVariable(function)
查詢所有的變量,包括父 scope
的變量。
scopeVariable := c.currentScopeVariable(function)
查詢當(dāng)前 scope
包含下級(jí)所有 scope
中的變量,這樣一減之后就能知道閉包變量了,然后將所有的閉包變量存放進(jìn)閉包函數(shù)中。
閉包賦值
之后在 return innerFun
處,將閉包變量的數(shù)據(jù)賦值到變量中。
閉包函數(shù)調(diào)用
func int(int) f2 = f1(); func int(int) f3 = f1();
在這里每一次賦值時(shí),都會(huì)把 f1()
返回函數(shù)復(fù)制到變量 f2/f3
中,這樣兩者所包含的閉包變量就不會(huì)互相影響。
在調(diào)用函數(shù)變量時(shí),判斷到該變量是一個(gè)函數(shù),則直接返回函數(shù)。
之后直接調(diào)用該函數(shù)即可。
函數(shù)式編程
接下來(lái)便可以利用 First class function
來(lái)試試函數(shù)式編程:
class Test{ int value=0; Test(int v){ value=v; } int map(func int(int) f){ return f(value); } } int square(int v){ return v*v; } int add(int v){ return v++; } int add2(int v){ v=v+2; return v; } Test t =Test(100); func int(int) s= square; func int(int) a= add; func int(int) a2= add2; println(t.map(s)); assertEqual(t.map(s),10000); println(t.map(a)); assertEqual(t.map(a),101); println(t.map(a2)); assertEqual(t.map(a2),102);
這個(gè)有點(diǎn)類似于 Java 中流的 map 函數(shù),將函數(shù)作為值傳遞進(jìn)去,后續(xù)支持匿名函數(shù)后會(huì)更像是函數(shù)式編程,現(xiàn)在必須得先定義一個(gè)函數(shù)變量再進(jìn)行傳遞。
除此之外在 GScript
中的 http
標(biāo)準(zhǔn)庫(kù)也利用了函數(shù)是一等公民的特性:
// 標(biāo)準(zhǔn)庫(kù):Bind route httpHandle(string method, string path, func (HttpContext) handle){ HttpContext ctx = HttpContext(); handle(ctx); }
在綁定路由時(shí),handle
便是一個(gè)函數(shù),使用的時(shí)候直接傳遞業(yè)務(wù)邏輯的 handle
即可:
func (HttpContext) handle (HttpContext ctx){ Person p = Person(); p.name = "abc"; println("p.name=" + p.name); println("ctx=" + ctx); ctx.JSON(200, p); } httpHandle("get", "/p", handle);
總結(jié)
總的來(lái)說閉包具有以下特性:
- 函數(shù)需要作為一等公民。
- 編譯期掃描出所有的閉包變量。
- 在返回閉包函數(shù)時(shí),為閉包變量賦值。
- 每次創(chuàng)建新的函數(shù)變量時(shí),需要將閉包數(shù)據(jù)復(fù)制進(jìn)去,這樣閉包變量才不會(huì)互相影響。
- 調(diào)用函數(shù)變量時(shí),需要判斷為函數(shù),而不是變量。
可以在 Playground 中體驗(yàn)閉包函數(shù)打印裴波那切數(shù)列的運(yùn)用。
到此這篇關(guān)于一文徹底理解Golang閉包實(shí)現(xiàn)原理 的文章就介紹到這了,更多相關(guān)Golang閉包內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go語(yǔ)言操作redis數(shù)據(jù)庫(kù)的方法
這篇文章主要介紹了Go語(yǔ)言操作redis數(shù)據(jù)庫(kù)的方法,本文通過實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-07-07Golang設(shè)計(jì)模式中的橋接模式詳細(xì)講解
橋接模式是一種結(jié)構(gòu)型設(shè)計(jì)模式,通過橋接模式可以將抽象部分和它的實(shí)現(xiàn)部分分離,本文主要介紹了GoLang橋接模式,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2023-01-01go語(yǔ)言打包的網(wǎng)頁(yè)wasm示例詳解
這篇文章主要為大家介紹了go打包網(wǎng)頁(yè)wasm示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪<BR>2022-09-09手把手教你vscode配置golang開發(fā)環(huán)境的步驟
這篇文章主要介紹了手把手教你vscode配置golang開發(fā)環(huán)境的步驟,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-03-03深入探討Go語(yǔ)言中的預(yù)防性接口為什么是不必要的
在Go語(yǔ)言中,有一種從其他語(yǔ)言帶來(lái)的常見模式:預(yù)防性接口,雖然這種模式在?Java?等語(yǔ)言中很有價(jià)值,但在Go中往往會(huì)成為反模式,本文我們就來(lái)深入探討一下原因2025-01-01golang?gorm的關(guān)系關(guān)聯(lián)實(shí)現(xiàn)示例
這篇文章主要為大家介紹了golang?gorm的關(guān)系關(guān)聯(lián)實(shí)現(xiàn)示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2022-04-04