这里写目录标题
一、Go闭包
1.闭包的定义
闭包是由函数及其相关的引用环境组合而成的实体(即:闭包=函数+引用环境)。
引用环境的定义
在函数式语言中,当内嵌函数体内引用到体外的变量时,将会把定义时涉及到的引用环境和函数体打包成一个整体(闭包)返回。现在给出引用环境的定义就容易理解了:引用环境是指在程序执行中的某个点所有处于活跃状态的约束(一个变量的名字和其所代表的对象之间的联系)所组成的集合。闭包的使用和正常的函数调用没有区别。
由于闭包把函数和运行时的引用环境打包成为一个新的整体,所以就解决了函数编程中的嵌套所引发的问题。当每次调用包含闭包的函数时都将返回一个新的闭包实例,这些实例之间是隔离的,分别包含调用时不同的引用环境现场。不同于函数,闭包在运行时可以有多个实例,不同的引用环境和相同的函数组合可以产生不同的实例。
2.fuctionValue
这些参数,返回值,变量 就是Function Value
Function Value本质上是一个指针,它指向一个结构体,结构体里有一个地址,地址指向函数指令的入口地址
3.闭包的原理
A.闭包的原理
到main函数执行阶段时
main函数的栈帧有两个局部变量,create函数会在堆上分配一个functionVal结构体fn,fn存放着地址指向函数入口,还有一个捕获列表捕获了一个变量c。如下图所示。每个fn有指向函数入口的地址和捕获变量,这就是闭包有状态的原因
B.闭包函数如何找到对应的捕获列表呢?
通过fuctionValue调用函数的时候,会把对应fucval结构体地址存入特定的寄存器。在闭包函数中可以通过寄存器拿到funcval结构体的地址,然后通过相应的便宜找到每一个捕获的变量。
C.捕获列表的不同情况的处理
1: 如B中的例子,被捕获的变量中只有初始化赋值。直接从栈中拷贝值到捕获列表中。
2: 除了初始化值,在其他地方被修改过(被捕获的是局部变量)
4.闭包的实现实例
闭包在go asm层面,会被转换为一个匿名结构体,结构体里面会存储闭包访问到的所有的上下文变量,并且必要的时候以地址的方式(call by name)进行存储。
➜ closure cat heap.go
package main
var globalA int = 8
var globalB int = 16
func main() {
f1 := funcClosure(globalA)
f1()
}
func funcClosure(i1111 int) func() int {
var t111 int = 20 + i1111
var t222 string = "tt"
return func() int {
i1111++
return i1111 + len(t222) + t111 + globalB
}
}
go tool compile -S heap.go > heap.s
...
//在funcClosure procedure里面可以找到
0x0043 00067 (heap.go:14) LEAQ type.noalg.struct { F uintptr; "".i1111 *int; "".t222 string; "".t111 int }(SB), DX
- 可以看到14行被转成了一个匿名的结构体,F就是闭包函数的入口地址, 并且存储了参数i的地址, 局部变量t222和t111的value。
- funcClosure会返回匿名结构体的变量F,也就是闭包函数的入口地址。然后在main里面进行调用。
- 那么针对value传递进去的变量,lifecycle可以认为是跟原来的变量分离了,交给匿名结构体的实例托管。
"".funcClosure STEXT size=228 args=0x10 locals=0x20
...
0x0021 00033 (heap.go:11) LEAQ type.int(SB), AX
0x0028 00040 (heap.go:11) PCDATA $2, $0
0x0028 00040 (heap.go:11) MOVQ AX, (SP)
0x002c 00044 (heap.go:11) CALL runtime.newobject(SB) //对上分配空间, AX=addr
0x0031 00049 (heap.go:11) PCDATA $2, $1
0x0031 00049 (heap.go:11) MOVQ 8(SP), AX
0x0036 00054 (heap.go:11) PCDATA $0, $1
0x0036 00054 (heap.go:11) MOVQ AX, "".&i1111+16(SP) //&i1111 + 16 = AX, 保存addr
0x003b 00059 (heap.go:11) MOVQ "".i1111+40(SP), CX
0x0040 00064 (heap.go:11) PCDATA $2, $0
0x0040 00064 (heap.go:11) MOVQ CX, (AX)
0x0043 00067 (heap.go:14) PCDATA $2, $2
0x0043 00067 (heap.go:14) LEAQ type.noalg.struct { F uintptr; "".i1111 *int; "".t222 string; "".t111 int }(SB), DX
0x004a 00074 (heap.go:14) PCDATA $2, $0
0x004a 00074 (heap.go:14) MOVQ DX, (SP)
0x004e 00078 (heap.go:14) CALL runtime.newobject(SB) // 新建闭包实例
0x0053 00083 (heap.go:14) PCDATA $2, $1
0x0053 00083 (heap.go:14) MOVQ 8(SP), AX
0x0058 00088 (heap.go:14) LEAQ "".funcClosure.func1(SB), CX //CX = F
0x005f 00095 (heap.go:14) MOVQ CX, (AX) //
0x0062 00098 (heap.go:14) PCDATA $2, $-2
0x0062 00098 (heap.go:14) PCDATA $0, $-2
0x0062 00098 (heap.go:14) CMPL runtime.writeBarrier(SB), $0
0x0069 00105 (heap.go:14) JNE 196
0x006b 00107 (heap.go:14) MOVQ "".&i1111+16(SP), CX // CX = addr ,取出之前新建的对象的地址
0x0070 00112 (heap.go:14) MOVQ CX, 8(AX) // 初始化 i1111
...
可以对于i1111这个变量,在funcClosure里面新建了一个堆上的变量,并且在闭包里面存储了这个变量的地址。
二、Go闭包的延申套路讨论
1.闭包与逃逸分析
闭包可能会导致变量逃逸到堆上来延长变量的生命周期,给 GC 带来压力。
package main
import "fmt"
func main() {
f := AddUpper()
fmt.Println(f(1))
fmt.Println(f(2))
fmt.Println(f(3))
}
func AddUpper() func(int) int {
var n int = 10
return func(x int) int {
n = n + x
return n
}
}
结果
11
13
16
可以执行一下如下指令:
D:\gowork\src\go_code\chapter02\escaptechar>go build -gcflags "-N -l -m" main.go
#command-line-arguments
.\main.go:22:6: moved to heap: n
.\main.go:23:9: func literal escapes to heap
.\main.go:15:13: ... argument does not escape
.\main.go:15:15: f(1) escapes to heap
.\main.go:16:13: ... argument does not escape
.\main.go:16:15: f(2) escapes to heap
.\main.go:17:13: ... argument does not escape
.\main.go:17:15: f(3) escapes to heap
如上可以发现n变量发现逃逸到堆空间
go build -gcflags "-N -l -m" closure
再举一个例子
package main
func main() {
f := fa(1)
g := fa(1)
println(f(1))
println(f(1))
println(g(1))
println(g(1))
}
func fa(a int) func(i int) int {
return func(i int) int {
println(&a, a)
a = a + 1
return a
}
}
2.闭包与外部函数的生命周期
内函数对外函数的变量的修改,是对变量的引用。共享一个在堆上的变量。
变量被引用后,它所在的函数结束,这变量也不会马上被销毁。相当于变相延长了函数的生命周期。
看个例子:
func AntherExFunc(n int) func() {
n++
return func() {
fmt.Println(n)
}
}
func ExFunc(n int) func() {
return func() {
n++
fmt.Println(n)
}
}
func main() {
myAnotherFunc:=AntherExFunc(20)
fmt.Println(myAnotherFunc) //0x48e3d0 在这儿已经定义了n=20 ,然后执行++ 操作,所以是21 。
myAnotherFunc() //21 后面对闭包的调用,没有对n执行加一操作,所以一直是21
myAnotherFunc() //21
myFunc:=ExFunc(10)
fmt.Println(myFunc) //0x48e340 这儿定义了n 为10
myFunc() //11 后面对闭包的调用,每次都对n进行加1操作。
myFunc() //12
}