Golang 閉包的實現

【導讀】什麼是閉包?什麼場景下會用閉包?本文對 go 語言中的閉包做了詳細介紹。

閉包是由函數及其相關引用環境組合而成的實體 (即:閉包 = 函數 + 引用環境)。

Go 中的閉包

閉包是函數式語言中的概念,沒有研究過函數式語言的用戶可能很難理解閉包的強大,相關的概念超出了本書的範圍。Go 語言是支持閉包的,這裏只是簡單地講一下在 Go 語言中閉包是如何實現的。

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

函數 f 返回了一個函數,返回的這個函數,返回的這個函數就是一個閉包。這個函數中本身是沒有定義變量 i 的,而是引用了它所在的環境(函數 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 引用的是不同的環境,在調用 i++ 時修改的不是同一個 i,因此兩次的輸出都是 1。函數 f 每進入一次,就形成了一個新的環境,對應的閉包中,函數都是同一個函數,環境卻是引用不同的環境。

變量 i 是函數 f 中的局部變量,假設這個變量是在函數 f 的棧中分配的,是不可以的。因爲函數 f 返回以後,對應的棧就失效了,f 返回的那個函數中變量 i 就引用一個失效的位置了。所以閉包的環境中引用的變量不能夠在棧上分配。

escape analyze

在繼續研究閉包的實現之前,先看一看 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 可以分析出變量的作用範圍,這是對垃圾回收很重要的一項技術。

閉包結構體

回到閉包的實現來,前面說過,閉包是函數和它所引用的環境。那麼是不是可以表示爲一個結構體呢:

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

事實上,Go 在底層確實就是這樣表示一個閉包的。讓我們看一下彙編代碼:

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)    // 這個結構體就是閉包的類型
...
CALL    ,runtime.new(SB)    // 接下來相當於 new(Closure)
PCDATA    $0,$-1
MOVQ    8(SP),AX
NOP    ,
MOVQ    $"".func·001+0(SB),BP
MOVQ    BP,(AX)                // 函數地址賦值給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 是另一個函數的函數地址,也就是 f 返回的那個函數。

小結

  1. Go 語言支持閉包

  2. Go 語言能通過 escape analyze 識別出變量的作用域,自動將變量在堆上分配。將閉包環境變量在堆上分配是 Go 實現閉包的基礎。

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

tiancaiamao.gitbooks.io/go-internals/content/zh/03.6.html

推薦關注「Linux 愛好者」,提升 Linux 技能

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/PxktbaSOXFEdvKZObhaHOA