3.6 闭包的实现

闭包是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。

Go中的闭包

闭包是函数式语言中的概念,没有研究过函数式语言的用户可能很难理解闭包的强大,相关的概念超出了本书的范围。Go语言是支持闭包的,这里只是简单地讲一下在Go语言中闭包是如何实现的。

  1. func f(i int) func() int {
  2. return func() int {
  3. i++
  4. return i
  5. }
  6. }

函数f返回了一个函数,返回的这个函数,返回的这个函数就是一个闭包。这个函数中本身是没有定义变量i的,而是引用了它所在的环境(函数f)中的变量i。

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

c1跟c2引用的是不同的环境,在调用i++时修改的不是同一个i,因此两次的输出都是1。函数f每进入一次,就形成了一个新的环境,对应的闭包中,函数都是同一个函数,环境却是引用不同的环境。

变量i是函数f中的局部变量,假设这个变量是在函数f的栈中分配的,是不可以的。因为函数f返回以后,对应的栈就失效了,f返回的那个函数中变量i就引用一个失效的位置了。所以闭包的环境中引用的变量不能够在栈上分配。

escape analyze

在继续研究闭包的实现之前,先看一看Go的一个语言特性:

  1. func f() *Cursor {
  2. var c Cursor
  3. c.X = 500
  4. noinline()
  5. return &c
  6. }

Cursor是一个结构体,这种写法在C语言中是不允许的,因为变量c是在栈上分配的,当函数f返回后c的空间就失效了。但是,在Go语言规范中有说明,这种写法在Go语言中合法的。语言会自动地识别出这种情况并在堆上分配c的内存,而不是函数f的栈上。

为了验证这一点,可以观察函数f生成的汇编代码:

  1. MOVQ $type."".Cursor+0(SB),(SP) // 取变量c的类型,也就是Cursor
  2. PCDATA $0,$16
  3. PCDATA $1,$0
  4. CALL ,runtime.new(SB) // 调用new函数,相当于new(Cursor)
  5. PCDATA $0,$-1
  6. MOVQ 8(SP),AX // 取c.X的地址放到AX寄存器
  7. MOVQ $500,(AX) // 将AX存放的内存地址的值赋为500
  8. MOVQ AX,"".~r0+24(FP)
  9. ADDQ $16,SP

识别出变量需要在堆上分配,是由编译器的一种叫escape analyze的技术实现的。如果输入命令:

  1. go build --gcflags=-m main.go

可以看到输出:

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

表示c逃逸了,被移到堆中。escape analyze可以分析出变量的作用范围,这是对垃圾回收很重要的一项技术。

闭包结构体

回到闭包的实现来,前面说过,闭包是函数和它所引用的环境。那么是不是可以表示为一个结构体呢:

  1. type Closure struct {
  2. F func()()
  3. i *int
  4. }

事实上,Go在底层确实就是这样表示一个闭包的。让我们看一下汇编代码:

  1. func f(i int) func() int {
  2. return func() int {
  3. i++
  4. return i
  5. }
  6. }
  7. MOVQ $type.int+0(SB),(SP)
  8. PCDATA $0,$16
  9. PCDATA $1,$0
  10. CALL ,runtime.new(SB) // 是不是很熟悉,这一段就是i = new(int)
  11. ...
  12. MOVQ $type.struct { F uintptr; A0 *int }+0(SB),(SP) // 这个结构体就是闭包的类型
  13. ...
  14. CALL ,runtime.new(SB) // 接下来相当于 new(Closure)
  15. PCDATA $0,$-1
  16. MOVQ 8(SP),AX
  17. NOP ,
  18. MOVQ $"".func·001+0(SB),BP
  19. MOVQ BP,(AX) // 函数地址赋值给Closure的F部分
  20. NOP ,
  21. MOVQ "".&i+16(SP),BP // 将堆中new的变量i的地址赋值给Closure的值部分
  22. MOVQ BP,8(AX)
  23. MOVQ AX,"".~r1+40(FP)
  24. ADDQ $24,SP
  25. RET ,

其中func·001是另一个函数的函数地址,也就是f返回的那个函数。

小结

  1. Go语言支持闭包
  2. Go语言能通过escape analyze识别出变量的作用域,自动将变量在堆上分配。将闭包环境变量在堆上分配是Go实现闭包的基础。
  3. 返回闭包时并不是单纯返回一个函数,而是返回了一个结构体,记录下函数返回地址和引用的环境中的变量地址。