golang websocket绑定用户_golang内核系列--深入理解函数闭包

f0629eee92daecdd4aa1576a08a49cd5.png

问题

闭包 是由函数及其相关引用环境组合而成的实体(即:闭包=函数+引用环境)。
“官方”的解释是:所谓“闭包”,指的是一个拥有许多变量和绑定了这些变量的环境的表达式(通常是一个函数),因而这些变量也是该表达式的一部分。

比如下面“斐波那契数列”闭包:

func fib() func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		return a
	}
}

调用如下
f00 := fib()
fmt.Println(f00(), f00(), f00(), f00(), f00())
输出结果是:1 1 2 3 5

golang里是如何做到这种闭包管理的呢?

闭包实现

我们先对闭包分3种场景:

  • 闭包里没有引用环境(变量生命周期很短,调用完即释放)
  • 闭包里引用全局变量(变量生命周期就是全局变量生命周期)
  • 闭包里引用局部变量(变量生命周期长,调用完不释放,下次调用会继续引用)

分别对3种场景以以下代码进行分析:

var y int

// 第一种场景
func fib01() func() int {
	return func() int {
		a, b := 0, 1
		a, b = b, a+b
		return a
	}
}

// 第二种场景
func fib00() func() int {
	return func() int {
		y++
		return y
	}
}

// 第三种场景
func fib3(x int) func() int {
	a, b := 0, 1
	return func() int {
		a, b = b, a+b
		x++
		return a+x
	}
}

保存上述文件为closure.go,然后在用汇编工具生成汇编代码。汇编参考golang内核系列–深入理解plan9汇编&实践
git:git@github.com:buptbill220/gotls.git使用如下命名生成汇编

go tool compile -S closure.go  > closure.S

我们打开closure.S找到对应的函数闭包位置

第一种场景fib01

59e1de5247b2eb92d7948bf9f404541a.png

然后找到 "".fib01.func1

209810a97a5cf02390bb4161e0cb6e60.png

闭包在全局区定义到代码段

"".fib01.func1·f SRODATA dupok size=8

我们可以看到:这种情况就是一种普通的函数,直接返回函数地址然后调用

0046b53b477bfa1916989fbfbd3bee2b.png

为了帮助理解这个实现,我们手动使用汇编根据一个函数地址调用函数

TEXT ·CallTest(SB),NOSPLIT,$0-8
    MOVQ arg+0(FP), AX
    CALL AX
    RET

func call_test() {
	fmt.Printf("call test")
}

func CallTest(uintptr)

x := call_test
CallTest(**(*(*uintptr))(unsafe.Pointer(&x)))

结果输出:call test

第二种场景fib00

19a4e0dcbbcbf7f771ad0d354c1ea34d.png

然后找到 “”.fib00.func1

37f960754e630dc72188f46a8ad0205a.png

y的位置

"".y SNOPTRBSS size=8

我们可以看到,第二种场景也是返回函数闭包的地址,只是闭包内部访问全局变量,并不做额外的工作。可以和场景1归纳为一类

第三种场景fib3

由于汇编较多,拆为2部分

510cdf7e1460863fb7f762e0ccfbfb0e.png

1746eeb86659722fcb71a6391e32af22.png

然后找到 “”.fib3.func1

66c79786ee769c343833493040b19ae3.png

通过上述分析,我们可以得知,所有引用局部变量,Golang在生成汇编是帮我们在堆上创建该变量的一个拷贝,并把该变量地址和函数闭包组成一个结构体,并把该结构体传出来作为返回值。
结构体形式如下:

type FF struct {
	F unitptr
	B *int
	A *int
	X *int // 如果X是string/[]int,那么这里应该为*string,*[]int
}

这个结构有个特点:结构里变量顺序和函数里引用顺序刚好相反。这是因为Golang为了保持物理地址顺序一致性的结果。

  • 栈的物理空间增长顺序是从大到小,栈里看到地址顺序是x>a>b
  • 而我们引用的是堆上物理空间增长顺序是从小到大,为了保持和栈上物理地址顺序一致,生成的结构顺序就是b、a、x

5fd21d554429a62351f1e45264bec34f.png

为了帮助理解这种实现,我们手动把函数闭包转换成结构并输出:

type FF struct {
	F unitptr
	b *int
	a *int
	x *int
}
f := fib3(0)
ptr := *(**FF)(unsafe.Pointer(&f))
fmt.Printf("ptr %v, %d, %d, %dn", ptr, *ptr.a, *ptr.b, *ptr.x)
fmt.Println(f(), f(), f(), f(), f())
fmt.Printf("ptr %v, %d, %d, %dn", ptr, *ptr.a, *ptr.b, *ptr.x)

自己手动调试看看

这里还有另一个特点:函数闭包内部本身是通过寄存器来访问引用环境的变量,在闭包调用前会把该结构地址提前放置寄存器(这里放到DX)
我们可以看看闭包调用前的代码:

9bc03380d9a9e0bd158940a753585992.png

总结

golang的函数闭包实现主要分为2中场景:

  • 闭包里没有引用环境&获取引用全局变量。这种场景下,其实现就是普通的函数,按照普通的函数调用方式执行闭包调用。
  • 闭包里引用局部变量。这种场景下,才是真正的闭包(函数+引用环境),并且以一个struct{FuncAddr, LocalAddr3, LocalAddr2, LocalAddr1}结构存储该闭包,等到调用闭包时,会把该结构地址提前放置一个寄存器,闭包内部通过该寄存器访问引用环境的变量
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值