Go語言之閉包


 

認識閉包

首先來看一段代碼:

  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 }

調試結果是這樣的:

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

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

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

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

 

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

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的技術實現的。如果輸入命令:

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

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

2、下面是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用法和閉包函數參數傳遞。

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

2、閉包里的非傳遞參數外部變量值是傳引用的,也就是閉包是地址引用。在閉包函數里那個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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM