GoLang之Function Value、闭包系列二(视频版)

GoLang之Function Value、闭包系列二(视频版)

注:本文以Go SDK v1.17进行讲解

1.FuncVal对象介绍

闭包复制的是原对象指针,这就很容易解释延迟引用现象
在汇编层 ,test 实际返回的是 FuncVal 对象,其中包含了匿名函数地址、闭包对象指针。当调 匿名函数时,只需以某个寄存器传递该对象即可
FuncVal { func_address, closure_var_pointer … }

package main

import "fmt"

func test() func() {
    x := 100
    fmt.Printf("x (%p) = %d\n", &x, x)

    return func() {
        fmt.Printf("x (%p) = %d\n", &x, x)
    }
}

func main() {
    f := test()
    f()
    /*
     x (0xc42007c008) = 100
    x (0xc42007c008) = 100
*/
}

go中函数是头等对象,可以作为参数传递,可以作函数返回值,也可以绑定到变量。go语言中称这样的参数、返回值或者变量为function value

image-20220307093305788

函数指令在编译期间生成,而function value本质是上一个指针,但是并不直接指向函数的指令入口。而是指向一个runtime.funcval结构体,这个结构体里只有一个地址,就是这个函数指令的入口地址

image-20220307093425901

2.FuncVal对象在函数调用栈中变化


func A(i int) {
	i++
	fmt.Println(i)
}
func B() {
	f1 := A
	f1(1)
}
func C() {
	f2 := A
	f2(1)
}
func main() {
	B()//输出2
	C()//输出2
}

下面看一个完整的例子,函数A被赋值给f1和f2两个变量,这种情况,编译器会做出优化,让f1和f2共用一个funcval结构体

image-20220307094136618

如果函数A的指令在这addr1那里

image-20220307094201919

编译阶段会在只读数据段分配一个funcval结构体

image-20220307094352008

fn指向函数A指令入口

image-20220307094430743

而funcval结构体它本身的起始地址addr2,会在执行阶段赋给f1和f2

image-20220307094457504

通过f1来执行函数,就会通过它存储的地址找到对应的funcval结构体,拿到函数入口地址,然后调转执行,参数为1,即i++i自增1,下一步输出2;
f2的调用完全相同;

image-20220307094635527

3.闭包对象在函数调用栈中变化(捕获局部变量只初始化赋值)

既然以以上的例子中函数入口地址就能调用 ,为什么要通过funcval结构体包装这个地址,然后使用一个二级指针来调用呢?这里主要是为了处理闭包的情况闭包的两个定义:
第一:必须要有在函数外部定义,但在函数内部引用的“自由变量”
第二:脱离了形成闭包的上下文,闭包也能照常使用这些自由变量

image-20220307095335267

函数create的返回值是一个函数,但这个函数内部使用了外部定义的变量c,即使create函数执行结束,但通过d与f2依然可以正常调用这个闭包函数,并使用定义在create函数内部的局部变量c,所以这里符合闭包的定义,通常这个变量c为捕捉变量;
闭包函数的指令自然也在编译阶段生成,但因为每个闭包对象都要保存自己的捕获变量,所以要到执行阶段才创建对应的闭包对象;
到执行阶段,main函数栈帧有两个局部变量

image-20220307100938106

然后是返回值空间

image-20220307101006865

到create函数帧这里,有一个局部变量c=2

image-20220307101212243

create函数会在堆上分配一个funcval结构体(addr1),fn指向闭包函数入口

image-20220307101339994

除此之外,还有一个捕获列表,这里只捕获了一个变量c

image-20220307101500101

然后这个结构体的起始地址就作为返回值写入返回值空间

image-20220307101657856

所以f1被赋为addr2

image-20220307101751651

下面再次调用create函数,他就会再次创建一个funcval结构体,同样接下来捕获变量c

image-20220307101912866

然后这个起始地址addr3作为返回值写入,最终f2被赋值为addr3

image-20220307102439858

通过f1和f2调用闭包函数,结汇找到各自对应的funcval结构体,拿到同一个函数入口,但是通过f1调用时要使用“ addr2 fn=addr1上面的c=2”这个捕获列表,通过f2调用时要使用“addr3 fn=addr1上面的c=2”这个捕获列表,这就是称闭包有状态的函数的原因

image-20220307102659540
image-20220307102718965

那闭包函数究竟如何找到对应的捕获列表呢,go语言中通过一个function value结构体调用函数时,会把对应的funcval结构体地址存入特定寄存器,例如amd64平台使用的是DX寄存器。这样在闭包函数中,就可以通过寄存器取出funcval结构体的地址,然后加上相应的偏移来找到每一个被捕获的变量。所以go语言中闭包就是有捕获列表的function value,而没有捕获列表的function value直接忽略这个寄存器的值就好了

image-20220307103134236

image-20220307103151445

最后来看看这个捕获列表,他可不是拷贝变量值这么简单;
被闭包捕获变量要在外层函数与闭包函数中表现一致,好像他们在使用同一个变量,为此Go语言编译器针对不同情况做了不同的处理

image-20220307103736548

最简单的情况就像上面那个例子,被捕获的变量除了初始化赋值外(c:=2),在任何地方都没有被修改过,所以直接拷贝值到捕获列表就ok了

4.闭包对象在函数调用栈中变化(捕获局部变量被初始化赋值且修改)

但是如果除了初始化赋值外还被修改过,那就要再做细分了,在下面这个例子中,被捕获的是局部变量i,而且除了初始化赋值(i:=0)外还被修改(i++)过

//返回的是一个func()函数类型的数组
func create() (fs [2]func()) {
	for i := 0; i < 2; i++ {
		fs[i] = func() {
			fmt.Println(i)
		}
	}
	return
}
func main() {
	fs := create()
	for i := 0; i < len(fs); i++ {
		fs[i]()
	}
	/*输出:
	2
	2
	*/
}

闭包函数指令入口addrf(红色圈圈的那个),main函数栈帧中,局部变量fs是一个长度为2的function value类型数组,返回值为零值

image-20220307104623865

到create函数栈帧,由于被闭包捕获并修改,局部变量i改为堆分配,在栈上只存一个地址

image-20220307104839840

第一次for循环,在堆上创建funcval结构体,捕获i的地址,这样闭包函数就和外层函数操作同一个变量了

image-20220307104940431

返回值第一个元素存储addr0,第一次for循环结束,i自增1

image-20220307105121332

第二次for循环开始,再次堆分配一个funcval,捕获变量i的地址,第二个元素存储addr1,第二次循环结束,i再次自增1

image-20220307105254667

此时满足循环退出条件,reate函数结束,把返回值拷贝到局部变量fs

image-20220307105403458

通过fs[0]调用函数时,把addr0存入寄存器,就可以找到addr0那个位置。闭包函数通过寄存器存储的地址加上偏移找到捕获变量i的地址

image-20220307105553699

fs[1]同理,通过f[s1]调用时,把add1存入寄存器,就可以找到addr1那个位置。闭包函数通过寄存器存储的地址加上偏移找到捕获变量i的地址.被捕获的地址都指向它,所以那里每次都会打印2

image-20220307110424044

闭包导致的局部变量堆分配,也是变量逃逸的一种场景

image-20220307110559497

5.闭包对象在函数调用栈中变化(捕获变量为参数)

如果有修改并被捕获的是参数,涉及到函数原型,就不能像局部变量那样处理了;
参数依然通过调用者栈帧传入,但是编译器会把栈上这个参数拷贝到堆上一份。然后外层函数和闭包函数都使用堆上分配的这一个

image-20220307110832240

6.闭包对象在函数调用栈中变化(捕获变量为返回值)

如果被捕获的是返回值,处理方式又有些不同;
调用者栈帧上依然会分配返回值的空间,不过闭包的外层函数会在堆上也分配一个,外层函数和闭包函数都使用堆上这个,但是在外层函数返回前,需要把堆上的返回值拷贝到栈上的返回值空间,处理方式虽然比较多样,但是目标只有一个,就是保持捕获变量在外层函数与闭包函数中的一致性

image-20220307110930125

7.Debug

7.1Debug(Function Value)

package main

import "fmt"

func A(i int) {
	i++
	fmt.Println(i)
}

func main() {
	f1 := A
	f1(1)
	f2 := A
	f2(1)
}

在这里插入图片描述在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

7.2Debug(捕获局部变量只初始化赋值)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

7.3Debug(捕获局部变量被初始化赋值且修改)

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GoGo在努力

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值