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