Go-闭包内存分析

一、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

}

总结

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值