Go 闭包(Closure)

1,021 阅读6分钟

1.不得不说的函数

1.1 函数: 第一公民

现代计算机系统进程执行基于堆栈运行的,对于支持函数编程的高级语言而言,其编译器不再需要对函数本身进行过多的转换就能在栈上运行,同时在程序代码实现对业务场景抽象化过程中,能将不同业务代码进行适当程度地解耦。

在Go中函数有很高的优先级,其大多数特性是基于函数进行“加工”实现的;例如类型方法、接口、错误处理均是基于函数支撑实现的。所以把函数称之为Go的第一公民实至名归,其表现特点有:

    1. 作为一种类型,函数类型变量既可作为入参、返参,又可以直接调用执行;
    2. 支持可变参数,可类比于切片【Go只有最后一个参数是可变的】;
    1. 支持多值返回,其实质是在栈空间地址上留出对应返参数量的空间存放数据【具体可分析函数的汇编代码】

1.2 函数的特点

1.2.1 定义:

func (point_Obj Point_Object) function_name (args ...type) (returnParam1 type, returnParam2 type, returnParam3 type){
	// body
}

function_name: 函数名

args: 不定传入参数

returnParam1: 返回参数1

1.2.2 多参数多返回值【不定参数】

//multiple the function is to calculate the result of two number which type is integer.
func multiple(x, y int) (r int, f bool) {
	z := x * y
	return z, true
}

汇编代码:

"".main STEXT size=80 args=0x0 locals=0x18 funcid=0x0
        0x0000 00000 (defineFunc.go:3)  TEXT    "".main(SB), ABIInternal, $24-0
        0x0000 00000 (defineFunc.go:3)  CMPQ    SP, 16(R14)
        0x0004 00004 (defineFunc.go:3)  PCDATA  $0, $-2
        0x0004 00004 (defineFunc.go:3)  JLS     73
        0x0006 00006 (defineFunc.go:3)  PCDATA  $0, $-1
        0x0006 00006 (defineFunc.go:3)  SUBQ    $24, SP
        0x000a 00010 (defineFunc.go:3)  MOVQ    BP, 16(SP)

由以上编译代码可知,Go使用栈帧改变值内容,来存放返回值,在调用函数之初传入的参数之上留了两个地址空位,从而实现多返回值。

1.2.3 参数传递本质 --- 到底是“值传递”还是“引用传递”

值传递(call by value)

其实质是传递拷贝值的副本;当函数在内部使用传入变量的过程中修改了其值,是不会改变原有变量的值。

func getValue1(x int) int {
	return x
}

引用传递(call by reference)

当有一天需求要求这个函数传入大量的数据,就不建议使用参数副本操作,只需要传递给函数一个指针,这个指针即指向传入参数的地址;但是注意这种参数传递会影响到后面程序调用这个变量的数据值,因为不变的是地址,而非地址上的值。

func getValue2(x *int) int {
	println("the x value:", x)
	return *x
}

运行上面两个栗子,结果如下:

func main() {
    var x int = 255
	v1 := getValue1(x)
	println("getValue1:", v1, "the addr:", &v1)
	v2 := getValue2(&v1)
	println("getValue2:", v2)
}

// ----- console -----
getValue1: 255 the addr: 0xc00003c768
the x value: 0xc00003c768
getValue2: 255

有以上可知,我们可以总结以下结论:

a.Go默认值拷贝,原有变量本身不会被改变;

b.引用传递对于参数传递而言,即一个指针变量,传递的是指针的地址,而这个指针变量与传递给函数的形参都是指向同一个地址,其本质仍是值拷贝。

1.2.4 匿名参数

var area = funca, b intint {
	return a*b
}

// call the above of the function.

area(1, 5)

2.“to be or not to be” --- 闭包

2.1 概念

回到我们今天要认识的“主角”闭包,闭包这个概念在众多高级语言中已被广泛应用到,同时也是函数式编程中重要的一环,因此关于闭包的定义存在以下公式:

闭包=函数+引用环境闭包 = 函数 + 引用环境

函数: 匿名函数

引用环境:编译器发现闭包,直接将闭包引用的外部变量在堆上分配空间;当闭包引用了函数的内部变量(即局部变量)时,每次调用的外部变量数据都会跟随闭包的变化而变化,闭包函数和外部变量是共享的

这里我们举个栗子说明下:

package main

import "fmt"

func f1() int {  // x:传入参数
	x := 5
	defer func() {
		x++
	}()
	return x
}

func f2() (x int) { // x: 返回参数,闭包存储的是x
	
	defer func() {
		x++
	}()
	return 5
}

func f3() (y int) { // 5: 同实例一相同
	x := 5
	defer func() {
		x++
	}()
	return x
}
func f4() (x int) {
	fmt.Println("x:", x)
	defer func(x int) { //
		x++
	}(x)
	return 5
}
func main() {
	fmt.Println(f1()) // 5
	fmt.Println(f2()) // 6
	fmt.Println(f3()) // 5
	fmt.Println(f4()) // 5 
}

2.2 作用

由上面的栗子我们可以得出闭包具备以下特点:

1.在函数调用中隐式地传递变量(共享变量)的值,减少直接声明全局变量的占用;

2.在调用相同函数的时候,会使返回的多个闭包共享该函数的局部变量

2.3 实现原理

同样,我们通过反汇编的技术手段进行闭包原理的原理分析:

package main

func f1(v string) func() {
	return func() {
		print(v)
	}
}

func main() {
	f := f1("go.go.go")
	f()
}

将f1() 进行反汇编,如下:

0x0014 00020 (simpleFunc.go:4)  MOVQ    BX, "".v+40(SP)
0x0019 00025 (simpleFunc.go:4)  MOVQ    AX, "".v+32(SP)
0x001e 00030 (simpleFunc.go:4)  LEAQ    type.noalg.struct { F uintptr; "".v string }(SB), AX
0x0025 00037 (simpleFunc.go:4)  PCDATA  $1, $0
0x0025 00037 (simpleFunc.go:4)  CALL    runtime.newobject(SB)
0x002a 00042 (simpleFunc.go:4)  LEAQ    "".f1.func1(SB), CX
0x0031 00049 (simpleFunc.go:4)  MOVQ    CX, (AX)
0x0034 00052 (simpleFunc.go:4)  MOVQ    "".v+40(SP), CX
0x0039 00057 (simpleFunc.go:4)  MOVQ    CX, 16(AX)
0x003d 00061 (simpleFunc.go:4)  PCDATA  $0, $-2
0x003d 00061 (simpleFunc.go:4)  CMPL    runtime.writeBarrier(SB), $0
0x0044 00068 (simpleFunc.go:4)  JNE     81
0x0046 00070 (simpleFunc.go:4)  MOVQ    "".v+32(SP), CX
0x004b 00075 (simpleFunc.go:4)  MOVQ    CX, 8(AX)
0x004f 00079 (simpleFunc.go:4)  JMP     95
0x0051 00081 (simpleFunc.go:4)  LEAQ    8(AX), DI
0x0055 00085 (simpleFunc.go:4)  MOVQ    "".v+32(SP), CX
0x005a 00090 (simpleFunc.go:4)  CALL    runtime.gcWriteBarrierCX(SB)
0x005f 00095 (simpleFunc.go:4)  PCDATA  $0, $-1
0x005f 00095 (simpleFunc.go:4)  PCDATA  $1, $-1
0x005f 00095 (simpleFunc.go:4)  MOVQ    16(SP), BP
0x0064 00100 (simpleFunc.go:4)  ADDQ    $24, SP
0x0068 00104 (simpleFunc.go:4)  RET
0x0069 00105 (simpleFunc.go:4)  NOP

1~2: 将堆栈顶指针(SP)初始化

3:把静态指针(SB)闭包类型数据结构放入到AX中,闭包类型代码如下:

type Closure struct {
	F uintptr
    v string
}
F: 函数指针
v:形式参数

注意:敲黑板,记笔记,重点,重点,重点,说三遍....

5:CALL调用函数,为SB上的闭包类型元信息创建闭包对象,并返回对应的对象地址,此处的runtime.newobject实际上是调用src/runtime/malloc.go。

6:将f1内部的匿名函数func1复制给计数寄存器CX

7:将CX存放的func1指针地址复制到闭包对象所在的地址上

8~20: 变量v复制闭包对象所在的地址上,复制闭包对象指针到函数返回值的地址上(SP)

3. 你不知道的逃逸分析(escape analysis)

什么是逃逸分析:

同众多优秀语言Java、Ruby、Python等高级语言一样,go语言具备了自动回收内存机制,Go编译器会从性能角度出发,决定是在堆上分配内存还是栈上分配,这个过程称为逃逸分析(escape analysis)。

闭包逃逸分析:

源码如下:

func GetAndIncrease() func() int {
	v := 0
	return func() int {
		v++
		return v
	}
}

func main() {
	val := GetAndIncrease()
	println(val()) // 1
	println(val()) // 2
}

输入命令行 go build -gcflags=-m xxx.go,查看编译逃逸状态如下:

# command-line-arguments
./simpleFunc.go:3:6: can inline GetAndIncrease
./simpleFunc.go:5:9: can inline GetAndIncrease.func1
./simpleFunc.go:12:23: inlining call to GetAndIncrease
./simpleFunc.go:5:9: can inline main.func1
./simpleFunc.go:13:13: inlining call to main.func1
./simpleFunc.go:14:13: inlining call to main.func1
./simpleFunc.go:4:2: moved to heap: v
./simpleFunc.go:5:9: func literal escapes to heap
./simpleFunc.go:5:9: func literal does not escape

由上面命令行,我们很容易分析出由于闭包函数访问了局部变量v,导致GetAndIncrease函数调用结束还一直存在函数中,因此局部变量v会逃逸到堆上moved to heap: v。