Go語言之閉包

 

認識閉包

首先來看一段代碼:html

 1 package main 2 3 import ( 4 "fmt" 5 ) 6 7 func squares() func() int { 8 var x int 9 return func() int { 10 x++ 11 return x * x 12 } 13 } 14 15 func main() { 16 f1 := squares() 17 f2 := squares() 18 19 fmt.Println("first call f1:", f1()) 20 fmt.Println("second call f1:", f1()) 21 fmt.Println("first call f2:", f2()) 22 fmt.Println("second call f2:", f2()) 23 }

調試結果是這樣的:git

代碼很簡單,就是定義一個square函數,返回值類型是func() int,返回的這個函數就是一個閉包。golang

那麼什麼是閉包呢? 閉包是函數和它所引用的環境,也就是閉包=函數+引用環境閉包

匿名函數雖然沒有定義x,可是它引用了他所在的環境(函數squares)中的變量x。f1跟f2引用的是不一樣的環境,在調用x++時修改的不是同一個x,所以兩個函數的第一次輸出都是1。函數squares每進入一次,就造成了一個新的環境,對應的閉包中,函數都是同一個函數,環境倒是引用不一樣的環境。函數

咱們在看一下上面的例子,發現變量x的生命週期不是由他的做用域所決定的,變量x在main函數中返回squares函數後依舊存在。變量x是函數squares中的局部變量,假設這個變量是在函數squares的棧中分配的,是不能夠的。由於函數squares返回之後,對應的棧就失效了,squares返回的那個函數中變量i就引用一個失效的位置了。因此閉包的環境中引用的變量不可以在棧上分配。ui

 

在繼續研究閉包的實現以前,先看一看Go的一個語言特性:spa

func f() *Cursor { var c Cursor c.X = 500 noinline() return &c }

Cursor是一個結構體,這種寫法在C語言中是不容許的,由於變量c是在棧上分配的,當函數f返回後c的空間就失效了。可是,在Go語言規範中有說明,這種寫法在Go語言中合法的。語言會自動地識別出這種狀況並在堆上分配c的內存,而不是函數f的棧上。指針

爲了驗證這一點,能夠觀察函數f生成的彙編代碼:調試

MOVQ $type."".Cursor+0(SB),(SP) // 取變量c的類型,也就是Cursor PCDATA $0,$16 PCDATA $1,$0 CALL ,runtime.new(SB) // 調用new函數,至關於new(Cursor) PCDATA $0,$-1 MOVQ 8(SP),AX // 取c.X的地址放到AX寄存器 MOVQ $500,(AX) // 將AX存放的內存地址的值賦爲500 MOVQ AX,"".~r0+24(FP) ADDQ $16,SP

識別出變量須要在堆上分配,是由編譯器的一種叫escape analyze的技術實現的。若是輸入命令:code

go build --gcflags=-m main.go

能夠看到輸出:

./main.go:20: moved to heap: c ./main.go:23: &c escapes to heap

表示c逃逸了,被移到堆中。escape analyze能夠分析出變量的做用範圍,這是對垃圾回收很重要的一項技術。

其實,Go經過escape analyza識別出變量的做用域,在閉包環境中,引用的變量不是在棧上分配,而是在堆中分配

返回閉包時並非單純的返回一個函數,而是返回一個結構體,記錄下函數返回地址和引用的環境中的變量地址,即:

type Closure struct { F func()() i *int }

 

閉包和普通函數調用的區別

看下面兩段代碼:

代碼片斷1:

package main import (   "fmt" ) func main() {    a := []int{1, 2, 3}   for _, value := range a {      fmt.Println(value)      defer p(value)    } } func p(value int) {   fmt.Println(value) }

運行結果:

1
2
3
3
2
1

代碼片斷1就是普通的函數調用,每次調用func p時,完成 value的值複製,而後打印,此時 value值複製了3次,分別是1,2,3。因爲defer是後進先出,因此執行變成3,2,1。

這裏或許對輸出結果感到有些意外,爲何正常輸出123後,又輸出321?搞清這點須要理解普通函數傳參方式和defer

一、咱們又知道,形參變量都是函數的局部變量,初始值由調用者提供的實參傳遞。而實參是按值傳遞的,即新闢內存拷貝變量值,函數接收到的是每一個實參的副本(slice、map、函數、通道和指針是引用傳遞,注意區別 )。

二、下面是go官方關於defer的解釋:

defer語句延遲執行一個函數,該函數被推遲到當包含它的程序返回時(包含它的函數 執行了return語句/運行到函數結尾自動返回/對應的goroutine panic)執行。

每次defer語句執行時,defer修飾的函數的返回值和參數取值會照常進行計算和保存,可是該函數不會執行。等到上一級函數返回前,會按照defer的聲明順序倒序執行所有defer的函數。defer的函數的任何返回值都會被丟棄。

基於以上兩點解釋,咱們就比較清楚了,defer修飾的函數會將傳入它的參數拷貝保存在本身的內存區域,等到函數返回時,纔開始執行。又因爲defer是先進後出的,因此最終打印結果是3,2,1。

 

代碼片斷2:

package main import (   "fmt" ) func main() {   a := []int{1, 2, 3}   for _, value := range a {     fmt.Println(value)     defer func() {      fmt.Println(value)     }() } }

運行結果:

1
2
3
3
3
3

 再來理解代碼片斷2。由上面咱們對defer的理解,函數返回時纔開始執行func(),但爲何輸出都是3呢?要搞清楚這個問題,還得理解for...range用法和閉包函數參數傳遞。

一、在Go的for…range循環中,Go始終使用值拷貝的方式代替被遍歷的元素自己,簡單來講,就是for…range中那個value,是一個值拷貝,而不是元素自己。也是說value是個局部變量,只是把元素賦值給該變量而已。

二、閉包裏的非傳遞參數外部變量值是傳引用的,也就是閉包是地址引用。在閉包函數裏那個value就是外部非閉包函數本身的參數,因此是至關於引用了外部的變量。

有了以上兩點的理解,再來理解代碼2的結果就容易多了。閉包是經過地址引用來引用環境中的變量value,所以每次只是把value的地址拷貝了一份兒,就這樣拷貝了三次。而執行到最後時value值爲3,因此打印了3次value地址指向的值,因此是3,3,3。

 

參考

《閉包的實現》:https://tiancaiamao.gitbooks.io/go-internals/content/zh/03.6.html

golang的閉包和普通函數調用區別》:https://studygolang.com/articles/356

相關文章
相關標籤/搜索