golang中的function value

一、function value

在golang中,函数是一等公民,将函数作为参数、变量或返回值的情况称为function valuefunction value本质上是一个指针,指向runtime.funcval,这个runtime.funcval结构体里只有一个地址,即函数指令的入口地址

funcval的struct定义如下

1
2
3
4
type funcval struct {
fn uintptr
// variable-size, fn-specific data here
}

二、无捕获列表的function value

对于无捕获列表的function value,如果多个变量关联到同一个函数,编译器会做出优化,让它们共用一个funcval结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func simpleFunc() {
}

type funcval struct {
fn uintptr
}

func main() {
var f1 = simpleFunc
var f2 = simpleFunc
fmt.Printf("%T\n", f1)
fmt.Printf("%T\n", f2)
fmt.Printf("%p\n", *(**funcval)(unsafe.Pointer(&f1)))
fmt.Printf("%p\n", *(**funcval)(unsafe.Pointer(&f2)))
fv1 := **(**funcval)(unsafe.Pointer(&f1))
fv2 := **(**funcval)(unsafe.Pointer(&f2))
fmt.Println(fv1)
fmt.Println(fv2)
}
// output:
// func()
// func()
// 0x1184298
// 0x1184298
// {18024256}
// {18024256}

需要注意的是f1,f2不是并不是指向函数的指针,而是指向函数指针的指针,即是一个二级指针,所以在进行强制转换时,要使用 *(**funcval)(unsafe.Pointer(&f1))的形式

从输出中可以看出f1,f2是共用了同一个funcval结构,且funcval指向的函数指令地址也相同

不直接通过函数入口地址,而是通过一个二级指针指向函数入口地址再调用主要是为了处理闭包的情况

三、闭包函数

闭包函数其实就是有捕获列表的funcval结构体,funcval加上偏移量可以找到每个捕获的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
func closure() func() int64 {
var c int64 = 2
return func() int64 {
return c
}
}

type funcval struct {
fn uintptr
i int64
}

func main() {
var f1 = closure()
var f2 = closure()
fmt.Printf("%T\n", f1)
fmt.Printf("%T\n", f2)
fmt.Printf("%p\n", *(**funcval)(unsafe.Pointer(&f1)))
fmt.Printf("%p\n", *(**funcval)(unsafe.Pointer(&f2)))
fv1 := **(**funcval)(unsafe.Pointer(&f1))
fv2 := **(**funcval)(unsafe.Pointer(&f2))
fmt.Println(fv1)
fmt.Println(fv2)
}

// output:
// func() int64
// func() int64
// 0xc0000b8280
// 0xc0000b8290
// {18025248 2}
// {18025248 2}

闭包函数closure中的局部变量c被捕获,closure函数会在堆上分配一个funcval结构体,fn执行函数指令地址,捕获列表捕获了变量c。

由输出可得,这次f1和f2对应了两个不同funcval结构,但两个funcval都指向相同的函数指令地址。f1,f2在调用时,会使用各自funcval结构中的捕获列表