Go語言 閉包的實(shí)現(xiàn)

2018-07-25 17:23 更新

閉包是由函數(shù)及其相關(guān)引用環(huán)境組合而成的實(shí)體(即:閉包=函數(shù)+引用環(huán)境)。

Go中的閉包

閉包是函數(shù)式語言中的概念,沒有研究過函數(shù)式語言的用戶可能很難理解閉包的強(qiáng)大,相關(guān)的概念超出了本書的范圍。Go語言是支持閉包的,這里只是簡單地講一下在Go語言中閉包是如何實(shí)現(xiàn)的。

func f(i int) func() int {
    return func() int {
        i++
        return i
    }
}

函數(shù)f返回了一個(gè)函數(shù),返回的這個(gè)函數(shù),返回的這個(gè)函數(shù)就是一個(gè)閉包。這個(gè)函數(shù)中本身是沒有定義變量i的,而是引用了它所在的環(huán)境(函數(shù)f)中的變量i。

c1 := f(0)
c2 := f(0)
c1()    // reference to i, i = 0, return 1
c2()    // reference to another i, i = 0, return 1

c1跟c2引用的是不同的環(huán)境,在調(diào)用i++時(shí)修改的不是同一個(gè)i,因此兩次的輸出都是1。函數(shù)f每進(jìn)入一次,就形成了一個(gè)新的環(huán)境,對(duì)應(yīng)的閉包中,函數(shù)都是同一個(gè)函數(shù),環(huán)境卻是引用不同的環(huán)境。

變量i是函數(shù)f中的局部變量,假設(shè)這個(gè)變量是在函數(shù)f的棧中分配的,是不可以的。因?yàn)楹瘮?shù)f返回以后,對(duì)應(yīng)的棧就失效了,f返回的那個(gè)函數(shù)中變量i就引用一個(gè)失效的位置了。所以閉包的環(huán)境中引用的變量不能夠在棧上分配。

escape analyze

在繼續(xù)研究閉包的實(shí)現(xiàn)之前,先看一看Go的一個(gè)語言特性:

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

Cursor是一個(gè)結(jié)構(gòu)體,這種寫法在C語言中是不允許的,因?yàn)樽兞縞是在棧上分配的,當(dāng)函數(shù)f返回后c的空間就失效了。但是,在Go語言規(guī)范中有說明,這種寫法在Go語言中合法的。語言會(huì)自動(dòng)地識(shí)別出這種情況并在堆上分配c的內(nèi)存,而不是函數(shù)f的棧上。

為了驗(yàn)證這一點(diǎn),可以觀察函數(shù)f生成的匯編代碼:

MOVQ    $type."".Cursor+0(SB),(SP)    // 取變量c的類型,也就是Cursor
PCDATA    $0,$16
PCDATA    $1,$0
CALL    ,runtime.new(SB)    // 調(diào)用new函數(shù),相當(dāng)于new(Cursor)
PCDATA    $0,$-1
MOVQ    8(SP),AX    // 取c.X的地址放到AX寄存器
MOVQ    $500,(AX)    // 將AX存放的內(nèi)存地址的值賦為500
MOVQ    AX,"".~r0+24(FP)
ADDQ    $16,SP

識(shí)別出變量需要在堆上分配,是由編譯器的一種叫escape analyze的技術(shù)實(shí)現(xiàn)的。如果輸入命令:

go build --gcflags=-m main.go

可以看到輸出:

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

表示c逃逸了,被移到堆中。escape analyze可以分析出變量的作用范圍,這是對(duì)垃圾回收很重要的一項(xiàng)技術(shù)。

閉包結(jié)構(gòu)體

回到閉包的實(shí)現(xiàn)來,前面說過,閉包是函數(shù)和它所引用的環(huán)境。那么是不是可以表示為一個(gè)結(jié)構(gòu)體呢:

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

事實(shí)上,Go在底層確實(shí)就是這樣表示一個(gè)閉包的。讓我們看一下匯編代碼:

func f(i int) func() int {
    return func() int {
        i++
        return i
    }
}


MOVQ    $type.int+0(SB),(SP)
PCDATA    $0,$16
PCDATA    $1,$0
CALL    ,runtime.new(SB)    // 是不是很熟悉,這一段就是i = new(int)    
...    
MOVQ    $type.struct { F uintptr; A0 *int }+0(SB),(SP)    // 這個(gè)結(jié)構(gòu)體就是閉包的類型
...
CALL    ,runtime.new(SB)    // 接下來相當(dāng)于 new(Closure)
PCDATA    $0,$-1
MOVQ    8(SP),AX
NOP    ,
MOVQ    $"".func·001+0(SB),BP
MOVQ    BP,(AX)                // 函數(shù)地址賦值給Closure的F部分
NOP    ,
MOVQ    "".&i+16(SP),BP        // 將堆中new的變量i的地址賦值給Closure的值部分
MOVQ    BP,8(AX)
MOVQ    AX,"".~r1+40(FP)
ADDQ    $24,SP
RET    ,

其中func·001是另一個(gè)函數(shù)的函數(shù)地址,也就是f返回的那個(gè)函數(shù)。

小結(jié)

  1. Go語言支持閉包
  2. Go語言能通過escape analyze識(shí)別出變量的作用域,自動(dòng)將變量在堆上分配。將閉包環(huán)境變量在堆上分配是Go實(shí)現(xiàn)閉包的基礎(chǔ)。
  3. 返回閉包時(shí)并不是單純返回一個(gè)函數(shù),而是返回了一個(gè)結(jié)構(gòu)體,記錄下函數(shù)返回地址和引用的環(huán)境中的變量地址。

links


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)