本文講介紹以下幾個(gè)內(nèi)容:
引入用GoLang語(yǔ)言寫(xiě)的幾個(gè)case;
介紹什么是閉包;
介紹什么是閉包的延遲綁定;
從閉包的延遲綁定講到GoLang的Go Routine的延遲綁定問(wèn)題;
I. 幾個(gè)有趣的Case
開(kāi)門見(jiàn)山,首先請(qǐng)各位看官們先看下面foo1()到foo7()一共7個(gè)函數(shù),然后回答后面的問(wèn)題。(一下子丟出7個(gè)函數(shù),請(qǐng)見(jiàn)諒。不過(guò),每個(gè)函數(shù)都非常簡(jiǎn)短,而本文接下來(lái)將圍繞這7個(gè)函數(shù)展開(kāi),因此,請(qǐng)各位看官老爺們耐心且看看題,活動(dòng)活動(dòng)腦細(xì)胞~)
case 1:
func foo1(x *int) func() { return func() { *x = *x + 1 fmt.Printf("foo1 val = %d ", *x) }}case 2:
func foo2(x int) func() { return func() { x = x + 1 fmt.Printf("foo2 val = %d ", x) }}case 3:
func foo3() { values := []int{1, 2, 3, 5} for _, val := range values { fmt.Printf("foo3 val = %d ", val) }}case 4:
func show(v interface{}) { fmt.Printf("foo4 val = %v ", v)}func foo4() { values := []int{1, 2, 3, 5} for _, val := range values { go show(val) }}case 5:
func foo5() { values := []int{1, 2, 3, 5} for _, val := range values { go func() { fmt.Printf("foo5 val = %v ", val) }() }}case 6:
var foo6Chan = make(chan int, 10)func foo6() { for val := range foo6Chan { go func() { fmt.Printf("foo6 val = %d ", val) }() }}case 7:
func foo7(x int) []func() { var fs []func() values := []int{1, 2, 3, 5} for _, val := range values { fs = append(fs, func() { fmt.Printf("foo7 val = %d ", x+val) }) } return fs}
Q1:
第一組實(shí)驗(yàn):假設(shè)現(xiàn)在有變量x=133,并創(chuàng)建變量f1和f2分別為foo1(&x)和foo2(x)的返回值,請(qǐng)問(wèn)多次調(diào)用f1()和f2()會(huì)打印什么?
第二組實(shí)驗(yàn):重新賦值變量x=233,請(qǐng)問(wèn)此時(shí)多次調(diào)用f1()和f2()會(huì)打印什么?
第三組實(shí)驗(yàn):如果直接調(diào)用foo1(&x)()和foo2(x)()多次,請(qǐng)問(wèn)每次都會(huì)打印什么?
Q2:
請(qǐng)問(wèn)分別調(diào)用函數(shù)foo3(),foo4()和foo5(),分別會(huì)打印什么?
Q3:
第一組實(shí)驗(yàn):如果“幾乎同時(shí)”往channelfoo6Chan里面塞入一組數(shù)據(jù)"1,2,3,5",foo6會(huì)打印什么?
第二組實(shí)驗(yàn):如果以間隔納秒(10^-9秒)的時(shí)間往channel里面塞入一組數(shù)據(jù),此時(shí)foo6又會(huì)打印什么?
第三組實(shí)驗(yàn):如果是微秒(10^-6秒)呢?如果是毫秒(10^-3秒)呢?如果是秒呢?
Q4:
請(qǐng)問(wèn)如果創(chuàng)建變量f7s=foo7(11),f7s是一個(gè)函數(shù)集合,遍歷f7s會(huì)打印什么?
接下來(lái),我們逐一來(lái)看這些問(wèn)題和對(duì)應(yīng)的foo函數(shù)。
II. case1~2:值傳遞(by value) vs. 引用傳遞(by reference)
子標(biāo)題好難起 >0<... ? 看到case1和case2的兩組函數(shù)foo1()和foo2(),相信各位看官就知道,其中一個(gè)知識(shí)點(diǎn)就是值傳遞和引用傳遞。 ? 其實(shí)呢,Go是沒(méi)有引用傳遞的,即使是foo1()在參數(shù)上加了*,內(nèi)部實(shí)現(xiàn)機(jī)制仍舊是值傳遞,只不過(guò)傳遞的是指針的數(shù)值。但是為了稱呼方便,下文會(huì)成為“引用傳遞”(為了區(qū)分正確的引用傳遞,這里特意加了引號(hào))。 ? 如下圖所示,我們的目的是傳遞X變量,于是我們創(chuàng)建了一個(gè)傳參地址(臨時(shí)地址變量),它存放了X變量的地址值,調(diào)用函數(shù)的時(shí)候給它這個(gè)傳參地址,函數(shù)呢,則會(huì)再創(chuàng)建一個(gè)入?yún)⒌刂罚莻鲄⒌刂返囊环菘截悺:瘮?shù)拿到了這個(gè)地址值,可以通過(guò)尋址拿到這個(gè)X變量,此時(shí)函數(shù)如果直接修改X變量可以認(rèn)為是“本地修改”或者“永久修改”了這個(gè)變量的數(shù)值。 ? 「舉個(gè)生活中的例子,比如一個(gè)叫做“函數(shù)”的人想尋找一個(gè)叫做“X”的人,函數(shù)跑過(guò)來(lái)問(wèn)知道X的我,我拿出地址簿,給他出示了X這個(gè)人的家庭地址,函數(shù)記性不太好,所以拿了一本本子把X的地址抄在了他自己的本子上。」 ? 這個(gè)例子中,我的那個(gè)記著X的家庭地址的地址簿,就是傳參地址;函數(shù)抄錄了X地址的本子,就是入?yún)⒌刂罚籜的家庭地址,就對(duì)應(yīng)了X變量的地址值。(哎,為什么講的這么細(xì)節(jié)了?) ? Golang的“引用傳遞” ? 話題似乎有點(diǎn)扯遠(yuǎn)了,拉回來(lái),我們?cè)賮?lái)看看foo1()和foo2()。 ? foo1()和foo2()的區(qū)別確實(shí)在于值傳遞和引用傳遞,但是這個(gè)并不是本文介紹的中心。本文要介紹的已經(jīng)在標(biāo)題上寫(xiě)明了:閉包(closure)。 ?
閉包(closure)
什么是閉包呢?摘用Wikipedia上的一句定義:
aclosureis a record storinga functiontogether withan environment.
閉包是由函數(shù)和與其相關(guān)的引用環(huán)境組合而成的實(shí)體 。
因此閉包的核心就是:函數(shù)和環(huán)境。其實(shí)這里就已經(jīng)可以回答本文題目的問(wèn)題:閉包究竟包了什么?答案是:函數(shù)和環(huán)境。但是相信部分看官們到這里依然不清楚:什么函數(shù)?什么環(huán)境? 函數(shù),指的是在閉包實(shí)際實(shí)現(xiàn)的時(shí)候,往往通過(guò)調(diào)用一個(gè)外部函數(shù)返回其內(nèi)部函數(shù)來(lái)實(shí)現(xiàn)的。內(nèi)部函數(shù)可能是內(nèi)部實(shí)名函數(shù)、匿名函數(shù)或者一段lambda表達(dá)式。用戶得到一個(gè)閉包,也等同于得到了這個(gè)內(nèi)部函數(shù),每次執(zhí)行這個(gè)閉包就等同于執(zhí)行內(nèi)部函數(shù)。 環(huán)境,Wikipedia上說(shuō)是與其(函數(shù))相關(guān)的引用環(huán)境,可以說(shuō)解釋地很精準(zhǔn)了。 具體地說(shuō),在實(shí)際中引用環(huán)境是指外部函數(shù)的環(huán)境,閉包保存/記錄了它產(chǎn)生時(shí)的外部函數(shù)的所有環(huán)境。但是這段話對(duì)于尚未理解閉包的同學(xué)來(lái)說(shuō)依舊是不友好的,聽(tīng)完還是懵懂的。這里嘗試做個(gè)更實(shí)用性的解釋:
如果外部函數(shù)的所有變量可見(jiàn)性都是local的,即生命周期在外部函數(shù)結(jié)束時(shí)也結(jié)束的,那么閉包的環(huán)境也是封閉的。
反之,那么閉包其實(shí)不再封閉,全局可見(jiàn)的變量的修改,也會(huì)對(duì)閉包內(nèi)的這個(gè)變量造成影響。
跳回foo1()和foo2()的例子,正好來(lái)解釋閉包的函數(shù)和環(huán)境。
func foo1(x *int) func() { return func() { *x = *x + 1 fmt.Printf("foo1 val = %d ", *x) }}func foo2(x int) func() { return func() { x = x + 1 fmt.Printf("foo1 val = %d ", x) }} // Q1第一組實(shí)驗(yàn)x := 133f1 := foo1(&x)f2 := foo2(x)f1() f2()f1()f2()// Q1第二組x = 233f1()f2()f1()f2()// Q1第三組foo1(&x)()foo2(x)()foo1(&x)()foo2(x)()定義了x=133之后,我們獲取得到了f1=foo1(&x)和f2=foo2(x)。這里f1f2就是閉包的函數(shù),也就是foo1()foo2()的內(nèi)部匿名函數(shù);而閉包的環(huán)境即外部函數(shù)foo1()foo2()的變量x(因?yàn)閮?nèi)部匿名函數(shù)引用到的相關(guān)變量只有x,因此這里簡(jiǎn)化為變量x)。 閉包的函數(shù)做的事情歸納為:1). 將環(huán)境的變量x自增1;2). 打印環(huán)境變量x。 閉包的環(huán)境則是其外部函數(shù)獲取到的變量x。 因此Q1第一組實(shí)驗(yàn)的答案為:
f1() // foo1 val = 134f2() // foo2 val = 134f1() // foo1 val = 135f2() // foo2 val = 135這是因?yàn)殚]包f1f2都保存了x=133時(shí)的整個(gè)環(huán)境,每次調(diào)用閉包f1f2都會(huì)執(zhí)行一次自增+打印的內(nèi)部匿名函數(shù)。因此第一次輸出都是(133+1=)134,第二次輸出都是(134+1=)135。 那么Q1第二組實(shí)驗(yàn)的答案呢?
f1() // foo1 val = 234f2() // foo2 val = 136f1() // foo1 val = 235f2() // foo2 val = 137有趣的事情發(fā)生了!f1的值居然發(fā)生了顯著性的變化!通過(guò)這組實(shí)驗(yàn),能夠更好地解釋其(函數(shù))相關(guān)的引用環(huán)境其實(shí)就是產(chǎn)生這個(gè)閉包的時(shí)候的外部函數(shù)的環(huán)境,因此變量x的可見(jiàn)性和作用域也與外部函數(shù)相同,又因?yàn)閒oo1是“引用傳遞”,變量x的作用域不局限在foo1()中,因此當(dāng)x發(fā)生變化的時(shí)候,閉包f1內(nèi)部也變化了。這個(gè)也正好是"反之,那么閉包其實(shí)不再封閉,全局可見(jiàn)的變量的修改,也會(huì)對(duì)閉包內(nèi)的這個(gè)變量造成影響"的證明。 Q1的第三組實(shí)驗(yàn)的答案:
foo1(&x)() // foo1 val = 236foo2(x)() // foo2 val = 237foo1(&x)() // foo1 val = 237foo2(x)() // foo2 val = 238foo2(x)() // foo2 val = 238因?yàn)閒oo1()返回的閉包都會(huì)修改變量x的數(shù)值,因此調(diào)用foo1()()之后,變量x必然增加1。而foo2()返回的閉包僅僅修改其內(nèi)部環(huán)境的變量x而對(duì)調(diào)用外部的變量x不影響,且每次調(diào)用foo2()返回的閉包是獨(dú)立的,和其他調(diào)用foo2()的閉包不相關(guān),因此最后兩次的調(diào)用,打印的數(shù)值都是相同的;第一次調(diào)用和第二次調(diào)用foo2()發(fā)現(xiàn)打印出來(lái)的數(shù)值增加了1,是因?yàn)閮纱握{(diào)用之間傳入的x的數(shù)值分別是236和237,而不是說(shuō)第二次在第一次基礎(chǔ)上增加了1,這點(diǎn)需要補(bǔ)充說(shuō)明。
III. case7:閉包的延遲綁定
hhh,是不是以為我會(huì)接著講case3,居然先提到了case7,意不意外驚不驚喜! 廢話不多說(shuō),看官們來(lái)瞅瞅下面調(diào)用f7()的時(shí)候分別會(huì)打印什么?
func foo7(x int) []func() { var fs []func() values := []int{1, 2, 3, 5} for _, val := range values { fs = append(fs, func() { fmt.Printf("foo7 val = %d ", x+val) }) } return fs}// Q4實(shí)驗(yàn):f7s := foo7(11)for _, f7 := range f7s { f7()}答案是:
foo7 val = 16foo7 val = 16foo7 val = 16foo7 val = 16是的,你沒(méi)有看錯(cuò),會(huì)打印4行,且都是16!是不是很驚喜! 相信已經(jīng)有很多同學(xué)在網(wǎng)上看到過(guò)類似的case,并且也早已知道結(jié)果了,不清楚的同學(xué)們現(xiàn)在也看到答案了。嗯,這就是大名鼎鼎的閉包延遲綁定問(wèn)題。網(wǎng)上的解釋其實(shí)有很多了,這里嘗試用之前對(duì)于閉包的環(huán)境的定義來(lái)解釋這個(gè)現(xiàn)象: “ 閉包是一段函數(shù)和相關(guān)的引用環(huán)境的實(shí)體。case7的問(wèn)題中,函數(shù)是打印變量val的值,引用環(huán)境是變量val。僅僅是這樣的話,遍歷到val=1的時(shí)候,記錄的不應(yīng)該是val=1的環(huán)境嗎? 上文在閉包解釋最后,還有一句話:閉包保存/記錄了它產(chǎn)生時(shí)的外部函數(shù)的所有環(huán)境。如同普通變量/函數(shù)的定義和實(shí)際賦值/調(diào)用或者說(shuō)執(zhí)行,是兩個(gè)階段。閉包也是一樣,for-loop內(nèi)部?jī)H僅是聲明了一個(gè)閉包,foo7()返回的也僅僅是一段閉包的函數(shù)定義,只有在外部執(zhí)行了f7()時(shí)才真正執(zhí)行了閉包,此時(shí)才閉包內(nèi)部的變量才會(huì)進(jìn)行賦值的操作。哎,如果這么說(shuō)的話,豈不是應(yīng)該拋出異常嗎?因?yàn)関al是一個(gè)比f(wàn)oo7()生命周期更短的變量啊? 這就是閉包的神奇之處,它會(huì)保存相關(guān)引用的環(huán)境,也就是說(shuō),val這個(gè)變量在閉包內(nèi)的生命周期得到了保證。因此在執(zhí)行這個(gè)閉包的時(shí)候,會(huì)去外部環(huán)境尋找最新的數(shù)值!你是不是不相信?來(lái)來(lái)來(lái),我們馬上寫(xiě)個(gè)臨時(shí)的case執(zhí)行下分分鐘就明白了:
臨時(shí)的case:
func foo0() func() { x := 1 f := func() { fmt.Printf("foo0 val = %d ", x) } x = 11 return f} foo0()() // 猜猜我會(huì)輸出什么?既然我說(shuō)會(huì)在執(zhí)行的時(shí)候去外部環(huán)境尋找最新的數(shù)值,那x的最新數(shù)值就是11呀,果然,最后輸出的就是11。 以上就是我對(duì)于閉包的延遲綁定的通俗版本解釋。:)
IV. case3~6:Go Routine的延遲綁定
case3、case4和case5不是閉包,case3只是遍歷了內(nèi)部的slice并且打印,case4是在遍歷時(shí)通過(guò)協(xié)程調(diào)用了打印函數(shù)打印,case5也是在遍歷slice時(shí)調(diào)用了內(nèi)部匿名函數(shù)打印。 Q2的case3問(wèn)題的答案先丟出來(lái):
func foo3() { values := []int{1, 2, 3, 5} for _, val := range values { fmt.Printf("foo3 val = %d ", val) }} foo3()//foo3 val = 1a//foo3 val = 2//foo3 val = 3//foo3 val = 5中規(guī)中矩,遍歷輸出slice的內(nèi)容:1,2,3,5。 Q2的case4問(wèn)題的答案再丟出來(lái):
func show(v interface{}) { fmt.Printf("foo4 val = %v ", v)}func foo4() { values := []int{1, 2, 3, 5} for _, val := range values { go show(val) }} foo4()//foo3 val = 2//foo3 val = 3//foo3 val = 1//foo3 val = 5嗯,因?yàn)镚o Routine的執(zhí)行順序是隨機(jī)并行的,因此執(zhí)行多次foo4()輸出的順序不一行相同,但是一定打印了“1,2,3,5”各個(gè)元素。 最后是Q2的case5問(wèn)題的答案:
func foo5() { values := []int{1, 2, 3, 5} for _, val := range values { go func() { fmt.Printf("foo5 val = %v ", val) }() }} foo5()//foo3 val = 5//foo3 val = 5//foo3 val = 5//foo3 val = 5居然都打印了5,驚不驚喜,意不意外?!相信看過(guò)子標(biāo)題的你,一定不意外了(捂臉)。是的,接下來(lái)就要講講Go Routine的延遲綁定: 其實(shí)這個(gè)問(wèn)題的本質(zhì)同閉包的延遲綁定,或者說(shuō),這段匿名函數(shù)的對(duì)象就是閉包。在我們調(diào)用go func() { xxx }()的時(shí)候,只要沒(méi)有真正開(kāi)始執(zhí)行這段代碼,那它還只是一段函數(shù)聲明。而在這段匿名函數(shù)被執(zhí)行的時(shí)候,才是內(nèi)部變量尋找真正賦值的時(shí)候。 在case5中,for-loop的遍歷幾乎是“瞬時(shí)”完成的,4個(gè)Go Routine真正被執(zhí)行在其后。矛盾是不是產(chǎn)生了?這個(gè)時(shí)候for-loop結(jié)束了呀,val生命周期早已結(jié)束了,程序應(yīng)該報(bào)錯(cuò)才對(duì)呀? 回憶上一章,是不是一個(gè)相同的情境?是的,這個(gè)匿名函數(shù)可不就是一個(gè)閉包嗎?一切就解釋通了:閉包真正被執(zhí)行的時(shí)候,for-loop結(jié)束了,但是val的生命周期在閉包內(nèi)部被延長(zhǎng)了且被賦值到最新的數(shù)值5。 不知道各位看官是否好奇,既然說(shuō)Go Routine執(zhí)行的時(shí)候比f(wàn)or-loop慢,那如果我在遍歷的時(shí)候增加sleep機(jī)制呢?于是設(shè)計(jì)了Q3實(shí)驗(yàn):
var foo6Chan = make(chan int, 10)func foo6() { for val := range foo6Chan { go func() { fmt.Printf("foo6 val = %d ", val) }() }}// Q3第一組實(shí)驗(yàn)go foo6()foo6Chan <- 1foo6Chan <- 2foo6Chan <- 3foo6Chan <- 5// Q3第二組實(shí)驗(yàn)foo6Chan <- 11time.Sleep(time.Duration(1) * time.Nanosecond)foo6Chan <- 12time.Sleep(time.Duration(1) * time.Nanosecond)foo6Chan <- 13time.Sleep(time.Duration(1) * time.Nanosecond)foo6Chan <- 15// Q3第三組實(shí)驗(yàn)// 微秒foo6Chan <- 21time.Sleep(time.Duration(1) * time.Microsecond)foo6Chan <- 22time.Sleep(time.Duration(1) * time.Microsecond)foo6Chan <- 23time.Sleep(time.Duration(1) * time.Microsecond)foo6Chan <- 25time.Sleep(time.Duration(10) * time.Second)// 毫秒foo6Chan <- 31time.Sleep(time.Duration(1) * time.Millisecond)foo6Chan <- 32time.Sleep(time.Duration(1) * time.Millisecond)foo6Chan <- 33time.Sleep(time.Duration(1) * time.Millisecond)foo6Chan <- 35time.Sleep(time.Duration(10) * time.Second)// 秒foo6Chan <- 41time.Sleep(time.Duration(1) * time.Second)foo6Chan <- 42time.Sleep(time.Duration(1) * time.Second)foo6Chan <- 43time.Sleep(time.Duration(1) * time.Second)foo6Chan <- 45time.Sleep(time.Duration(10) * time.Second)// 實(shí)驗(yàn)完畢,最后記得關(guān)閉channelclose(foo6Chan)嘗試執(zhí)行了多次,第一組答案如下:
foo6 val = 5/3foo6 val = 5foo6 val = 5foo6 val = 5絕大部分時(shí)候執(zhí)行出來(lái)都是5。 第二組答案如下:
foo6 val = 15/13/11/12foo6 val = 15/13foo6 val = 15foo6 val = 15絕大部分時(shí)候執(zhí)行得到的都是15。 第三組答案如下:
// 微秒foo6 val = 23/21foo6 val = 23/22foo6 val = 25/23foo6 val = 25// 毫秒foo6 val = 31foo6 val = 32foo6 val = 33foo6 val = 35// 秒foo6 val = 41foo6 val = 42foo6 val = 43foo6 val = 45毫秒和秒的兩組非常確定,順序輸出。但是微妙就不一定了,有時(shí)候是順序輸出,大部分時(shí)候是隨機(jī)輸出如“22,22,23,25”或者“21,22,25,25”之類的。 可見(jiàn),Go Routine的匿名函數(shù)從定義到執(zhí)行,耗時(shí)時(shí)間在微妙上下。于是又增加了一個(gè)臨時(shí)的case測(cè)試了其真正的耗時(shí)大約是多少。
又一個(gè)臨時(shí)的case:
func foo8() { for i := 1; i < 10; i++ { curTime := time.Now().UnixNano() go func(t1 int64) { t2 := time.Now().UnixNano() fmt.Printf("foo8 ts = %d us ", t2-t1) }(curTime) }} foo8()執(zhí)行下來(lái)發(fā)現(xiàn)耗時(shí)在5微秒~60微秒之間不等。 但是,以上的實(shí)驗(yàn)數(shù)據(jù)都是從我的iMac本子上得到的,該本子的CPU是i7-7700K 4.2GHz;我又放在筆記本上(CPU為i5-8250U 1.6GHz 1.8GHz)運(yùn)行了下,發(fā)現(xiàn)居然耗時(shí)是0微秒!起初我懷疑是時(shí)間精度的問(wèn)題,于是把t1和t2時(shí)間都打印出來(lái),精度是可以達(dá)到納秒的。抱著仍舊不信的想法,重新運(yùn)行了第三組實(shí)驗(yàn),每一個(gè)都是順序輸出的! 好吧,回頭再說(shuō)我的iMac的問(wèn)題。現(xiàn)在只需要記住一點(diǎn):Go Routine的匿名函數(shù)的延遲綁定本質(zhì)就是閉包,實(shí)際生成中注意下這種寫(xiě)法~
寫(xiě)在后面
最后,閉包是個(gè)常見(jiàn)的玩意兒,但是實(shí)際代碼中不太建議使用,一不小心寫(xiě)了個(gè)內(nèi)存泄漏查都查不到。特別是不要為了炫技故意寫(xiě)個(gè)閉包,實(shí)在沒(méi)有必要。
-
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
7085瀏覽量
89217 -
函數(shù)
+關(guān)注
關(guān)注
3文章
4338瀏覽量
62767 -
閉包
+關(guān)注
關(guān)注
0文章
4瀏覽量
2070
原文標(biāo)題:Golang:“閉包(closure)”到底包了什么?
文章出處:【微信號(hào):magedu-Linux,微信公眾號(hào):馬哥Linux運(yùn)維】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論