You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.
---
title: "闭包"
---
# 闭包
C语言中函数名称就是函数的首地址。Go语言中函数名称跟C语言一样, 函数名指向函数的首地址, 即函数的入口地址。从前面《**[基础篇-函数-一等公民]({{< relref " function / first-class " > }})**》那一章节我们知道Go 语言中函数是一等公民,它可以绑定变量,作函数参数,做函数返回值,那么它底层是怎么实现的呢?
我们先来了解下 `Function Value` 这个概念。
## Function Value
Go 语言中函数是一等公民, 函数可以绑定到变量, 也可以做参数传递以及做函数返回值。Golang把这样的参数、返回值、变量称为**Function value**。
Go 语言中**Function value**本质上是一个指针,但是其并不直接指向函数的入口地址,而是指向的`runtime.funcval`(**[runtime/runtime2.go](https://github.com/cyub/go-1.14.13/blob/master/src/runtime/runtime2.go#L195-L198)**)这个结构体。该结构体中的fn字段存储的是函数的入口地址:
```go
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}
```
我们以下面这段代码为例来看下**Function value**是如何使用的:
```go
func A ( i int ) {
i ++
fmt . Println ( i )
}
func B () {
f1 := A
f1 ( 1 )
}
func C () {
f2 := A
f2 ( 2 )
}
```
上面代码中, 函数A被赋值给变量f1和f2, 这种情况下编译器会做出优化, 让f1和f2共用一个funcval结构体, 该结构体是在编译阶段分配到数据段的只读区域(.rodata)。如下图所示那样, f1和f2都指向了该结构体的地址addr2, 该结构体的fn字段存储了函数A的入口地址addr1:
{{< figure src = "https://static.cyub.vip/images/202105/fuc_var.png" width = "400px" class = "text-center" > }}
为什么f1和f2需要通过了一个二级指针来获取到真正的函数入口地址, 而不是直接将f1, f2指向函数入口地址addr1。关于这个原因就涉及到Golang中闭包设计与实现了。
## 闭包
** 闭包(Closure)** 通俗点讲就是能够访问外部函数内部变量的函数。像这样能被访问的变量通常被称为捕获变量。
闭包函数指令在编译阶段生成,但因为每个闭包对象都要保存自己捕获的变量,所以要等到执行阶段才创建对应的闭包对象。我们来看下下面闭包的例子:
```go
package main
func A () func () int {
i := 3
return func () int {
return i
}
}
func main () {
f1 := A ()
f2 := A ()
print ( f1 ())
pirnt ( f2 ())
}
```
上面代码中当执行main函数时, 会在其栈帧区间内为局部变量f1和f2分配栈空间, 当执行第一个A函数时候, 会在其栈帧空间分配栈空间来存放局部变量i, 然后在堆上分配一个funcval结构体( 其地址假定addr2), 该结构体的fn字段存储的是A函数内那个闭包函数的入口地址( 其地址假定为addr1) 。A函数除了分配一个funcval结构体外, 还会挨着该结构体分配闭包函数的变量捕获列表, 该捕获列表里面只有一个变量i。由于捕获列表的存在, 所以说**闭包函数是一个有状态函数**。
当A函数执行完毕后, 其返回值赋值给f1, 此时f1指向的就是地址addr2。同理下来f2指向地址addr3。f1和f2都能通过funcval取到了闭包函数入口地址, 但拥有不同的捕获列表。
当执行f1()时候, Go 语言会将其对应funcval地址存储到特定寄存器( 比如amd64平台中使用rax寄存器) , 这样在闭包函数中就可以通过该寄存器取出funcval地址, 然后通过偏移找到每一个捕获的变量。由此可以看出来**Golang中闭包就是有捕获列表的Function value**。
根据上面描述,我们画出内存布局图:
{{< figure src = "https://static.cyub.vip/images/202105/func_clo.png" width = "400px" class = "text-center" > }}
若闭包捕获的变量会发生改变,编译器会智能的将该变量逃逸到堆上,这样外部函数和闭包引用的是同一个变量,此时不再是变量值的拷贝。这也是为什么下面代码总是打印循环的最后面一个值。
```go
package main
func main () {
fns := make ([] func (), 0 , 5 )
for i := 0 ; i < 5 ; i ++ {
fns = append ( fns , func () {
println ( i )
})
}
for _ , fn := range fns { // 最后输出5个5, 而不是0, 1, 2, 3, 4
fn ()
}
}
```
感兴趣的可以仿造上图,画出上面代码的内存布局图。重点关注闭包函数捕获的不是值拷贝,而是引用一个堆变量。