1.不得不说的函数
1.1 函数: 第一公民
现代计算机系统进程执行基于堆栈运行的,对于支持函数编程的高级语言而言,其编译器不再需要对函数本身进行过多的转换就能在栈上运行,同时在程序代码实现对业务场景抽象化过程中,能将不同业务代码进行适当程度地解耦。
在Go中函数有很高的优先级,其大多数特性是基于函数进行“加工”实现的;例如类型方法、接口、错误处理均是基于函数支撑实现的。所以把函数称之为Go的第一公民实至名归,其表现特点有:
-
- 作为一种类型,函数类型变量既可作为入参、返参,又可以直接调用执行;
- 支持可变参数,可类比于切片【Go只有最后一个参数是可变的】;
-
- 支持多值返回,其实质是在栈空间地址上留出对应返参数量的空间存放数据【具体可分析函数的汇编代码】
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 = func(a, b int)int {
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。