开发者学堂课程【Go 语言核心编程 - 基础语法、数组、切片、Map :函数调用机制底层剖析】学习笔记,与课程紧密联系,让用户快速学习知识。
课程地址:https://developer.aliyun.com/learning/course/625/detail/9597
函数调用机制底层剖析
函数的调用机制
学习 GO 语言或者其他编程语言的过程中,函数的调用机制是非常重要的。
如果把函数调用机制理解清楚,在后面学习递归的过程中会更加轻松。
首先找一个通俗易懂的例子,以方便理解函数调用机制。例如拉登给特工小组下达一个命令让特工炸掉白宫,特工小组能否把白宫炸掉最终会有一个结果返回给拉登。
其中就有一个下达命令给特工小组,特工小组返回结果给拉登的一个过程。这是在现实生活中的一种情况。在函数调用机制中,拉登相当于程序员。
程序员去调用方法,方法就是去完成一段功能的集合,执行完毕之后会返回结果,这是一个较为通俗易懂的例子。但是不够本质换另外一种方式来理解,也就是从代码的角度来理解函数的调用机制。为了更好的理解函数的调用过程,看两个案例并画出示意图。举2个例子。
第一个例子是传入一个数加1,第二个例子是计算2个数并返回。传入一个数,并把它加1是很简单的一个过程,主要就在于如何实现。
实现方法如下:
案例一:
打开 vs code 的新建一个文件夹取名字叫 Funcdemo02 ,在其中新建一个文件叫main.go。
前面有一段代码是它的架构。接下来编写一个函数,这个函数的名字命名为 test ,接收一个数 N1,数据类型为 int 类型。
拿到这个数之后,要对它加1,就可以通过 N1=N1+1,然后输出这个运算过后的值,
代码如下:
Func test(n1 int){
n1=n1+1
Fmt.println(‘’n1=’’,n1)
}
写好代码后在主函数中调用它。一个标准的调用方式是,先定义一个变量n1。
定义两个一模一样的变量是为了看到在底层代码之中在内存里面是什么样的机制在运算和操作。然后给 N11个值。用推导公式定义一个值叫 N1。
把 N1 传进去。代码如下:
Func main(){
n1:=10
test(n1)
}
运行代码之后输出结果是11。但是要在后面再输出一个值引起思考。
在最后加入一行代码:
Fmt.println(“n1=”,n1)
为了区别2个N1。
如果同时输出,那么不会知道是 test 里面输出的 n1 还是 main 函数里面输出的n1。可以在输出结果中把 test 输出的 N1 命名为 test()N1,把 main 里面输出的N1命名为 main()N1。
先不去运行结果先从内存的角度分析结果。分析过后再进行执行。如果执行的结果和分析结果相同,证明分析到位。
在 GO long 中,函数调用的机制的底层分析。首先需要强调一个大致的概况,在程序运行的时候,在内存里会把进程分成3个部分,分别是栈区、堆区和代码区,不同的语言会有微小的差别。
一般来说栈区就是函数在调用过程中基本数据变量分配的地方。 GO 语言中,编译器有逃逸分析的特点,所以说不一定所有的数据类型都分布在栈区中。
这个划分不是物理上的划分,而是逻辑上的划分。所以基本数据类型一般来说分配到栈区中。
这是 GO 语言的设计者在其中做了优化也就是说,基本数据类型不一定分布在栈区,但是一般来说都分布到栈区中了。
一般来说引用数据类型分布在堆区。比如 map 分布在堆区,Java 里面的对象都分布在堆区中。代码区也就是代码在执行过程中,这个指令存取的地方。
CPU 如何得知加减乘除,就是因为有代码。代码需要有地方存。如果不存储的话,计算机就不知道要读取什么东西,所以代码存放到代码区中。有了划分之后再来考虑这个过程就较为简单了。
Func test(n1 int){
n1=n1+1
Fmt.println(‘’n1=’’,n1)
}
Func main(){
n1:=10
test(n1)
}
进入到刚才书写的代码里的 Chapter 之中的 Funk demo 02中,输入 cmd ,控制台输出的东西就认为是画在图中的内容。首先,代码执行到N1时安排了一个类型推导的值,此时内存中划分了一个空间在战区中,为了区分 main 里面的空间和 test的空间不同,所以给它取名叫 main 栈区。
可以理解为在 main 里面的基本数据类型都会分配到 main 栈区中。这个空间也是逻辑上的空间,而不是物理上的空间。每个函数在调用时都有一个数据的导入,也会分配一个空间给他。
在执行 n1:=10后,在 main 战区中产生了一个含有N1变量的空间。执行完之后,在test 中调用了 test 中的 N1。此时就是调用函数,根据操作系统的原理和技术规定,当一段代码去调用一个代码时,会再开辟一个空间给代码。
这个空间还是逻辑空间,就可以认为又产生了一个战区。在真实的内存当中是没有main 战区和 test 战区的区分,但是为了能够容易理解,所以做了一个区分。
在真实的内存中,其实也有对 main 和 test 做区分,但是由于涉及到编译原理和编译器的知识,所以不展开叙述。调用 test 后,执行到
Func test(n1 int){
n1=n1+1
Fmt.println(‘’n1=’’,n1)
}
位置。
此时会把 N1 的值传给 test 里面的 N1。此时在 test 里面也会存在一个 N1,它的值就等于 main 里面传过来的值,换而言之也是10,但是可以从图中清晰地看到N1,虽然都叫 N1,但其实是独立的。
理解了这个原理,那么在理解函数的调用机制时,也就简单了很多。之后,又执行了 N1=N1+1,此时 N1 等于11。换值之后执行到了 Fmt.println(‘’test n1=’’,n1)句话。终端会输出 test ()N1=11。
执行完 test 函数后,如果有返回语句就返回,如果没有返回语句就结束了。没有返回语句,就回到原来的位置,可以认为有一个 CPU 一样的东西在不停地调动。此时,test 战区空间消失,编译器会回收这个空间。
此时继续执行 Fmt.println(‘’main n1=’’,n1)句话,终端也会继续输出main()n1=10 ,这句话执行完之后整个战执行完毕。
内存给这个程序分布的空间也就被全部回收了,所以代码执行完毕终端会出现2句话:
Test()n1=11
main()n1=10
此时执行代码, GO Run main,执行结果与分析结果相同。
对以上代码的运行进行几点说明:
(1) 在调用一个函数时,会给该函数分配一个新的空间,可以理解为新的栈。编译器会通过自身的处理,让这个新的空间和其他的占的空间进行区分。
(2) 在每个函数对应的栈中,数据空间是独立的,不会混淆在一起。如果有引用变量,就有可能混淆在一起。
(3) 当一个函数调用完毕后,战区中为其分配的空间也会随之而被回收,程序会销毁这个函数对应的栈空间。
案例二:
Func test(n1 int){
n1=n1+1
Fmt.println(‘’n1=’’,n1)
}
既然要计算2个数的和,那就必须有2个数 N1 和 N2,两个数都是整型。
要把2个数算出来都返回,所以要有一个 sum=N1+N2。这句话的意思就是把 N1 和 N2 加起来,再赋给一个 sum 的变量。然后 Return sum ,这两句话是可以合在一起的,看个人习惯,也无所谓高低。可以写为 Return N1+N2。
在在代码最后写 sum 等于 get Some。把结果输出在其中写 main sum,此时前后2句话的输出都是一样的。 Return 可以理解为当他把 get Sum 执行完之后,就会把 sum 返回给调用者,也就是当函数有 Return 语句时,就是将结果返回给调用者,即谁调用我就返回给谁。
传2个数进去,10和20。此时Fmt.println(‘’getsum sum=’’,sum)结果为30,return sum 也就是 return 30,因为执行到此时进行了替换。
Sum:=getsum(10,20)可以理解为 sum:=30,因为执行 return sum 之后把结果返回给 Sum:=getsum(10,20)了,
总体代码如下:
Func test(n1 int){
n1=n1+1
Fmt.println(‘’test()n1=’’,n1)
}
Func getsum(n1 int,n2 int){
Sum:=n1+n2
Fmt.println(“getsum sum=”,sum)
Return sum
}
Func main(){
n1:=10
test(n1)
Fmt.println(“main n1=”,n1)
Sum:=getsum(10,20)
Fmt.println(“main sum=”,sum)
}
执行代码后发现代码有小问题,因为在执行变量时少了一个东西,int指定参数列表。
Func test(n1 int){
n1=n1+1
Fmt.println(‘’test()n1=’’,n1)
}
Func getsum(n1 int,n2 int)int{
Sum:=n1+n2
Fmt.println(“getsum sum=”,sum)
Return sum
}
Func main(){
n1:=10
test(n1)
Fmt.println(“main n1=”,n1)
Sum:=getsum(10,20)
Fmt.println(“main sum=”,sum)
}
此时,代码没有问题,执行之后的结果为:
Test() n1=11
Main()n1=10
Getsum n1=30
Main sum=30
最后两个值相同的原因是因为把结果返回了。
代码中使用了 return 语句,下面是对 Return 语句的解释。 GO 函数中支持返回多个值,这一点是其他编程语言没有的。
函数带 Return 的基本语句如下:
Func 函数名(形参列表)(返回值列类型列表)
语句
Return(返回值列表)
Func 是关键字,形参列表就是需要传的东西,返回值类型列表就是有哪些返回值它的类型,语句就是这个函数的功能。Return 语句就是返回值列表。
需要注意的是,
(1) 如果返回多个值在接收时希望忽略返回某个返回值,则使用_符号表示站位忽略
(2) 如果返回值只有一个返回值类型列表可以不写,也可以写,如果有多个就必须写。
案例:请编写函数,可以计算2个数的和和差,并返回结果。
此时要返回2个值,所以和刚才的内容有差异。
定义2个变量,N1,N2,计算和用 sum=N1+N2。Sub 就是N1-N2,Return sum,sub。写完之后,调用函数 getsumandsub ,传入1,2两个值,第一个值交给 result1,第二个值交给 result 2,最后打印出这个结果,输出,result 1和result 2。
代码如下:
Func test(n1 int){
n1=n1+1
Fmt.println(‘’test()n1=’’,n1)
}
Func getsum(n1 int,n2 int)int{
Sum:=n1+n2
Fmt.println(“getsum sum=”,sum)
Return sum
}
Funct getsumandsub(n1 int,n2 int)(int int){
Sum:=n1+n2
Sub:=n1-n2
Return sum,sub
}
Func main(){
n1:=10
test(n1)
Fmt.println(“main n1=”,n1)
Sum:=getsum(10,20)
Fmt.println(“main sum=”,sum)
Res1,res2:=getsumandsub(1,2)
Fmt.println(“res1=\v res2=\n=”,res1,res2)
}
执行之后,发现多写了一个N,因为没有用IdeA工具,所以是靠自己代码写的。更改之后运行可以看到结果 result1=3 result 2=-1。
在某些情况下可能只需要某一个值,如果返回的是和和差,但是可能只想要和不想要差或者只想要差不想要和,可以使用_来表示站位忽略某个值。
案例:
_,res3:=getsumandsub(1,2),此时只得到一个差,将 result 3输出。决心代码之后,结果为 result,3=-6。代码如下:
Func test(n1 int){
n1=n1+1
Fmt.println(‘’test()n1=’’,n1)
}
Func getsum(n1 int,n2 int)int{
Sum:=n1+n2
Fmt.println(“getsum sum=”,sum)
Return sum
}
Funct getsumandsub(n1 int,n2 int)(int int){
Sum:=n1+n2
Sub:=n1-n2
Return sum,sub
}
Func main(){
n1:=10
test(n1)
Fmt.println(“main n1=”,n1)
Sum:=getsum(10,20)
Fmt.println(“main sum=”,sum)
Res1,res2:=getsumandsub(1,2)
Fmt.println(“res1=\v res2=\n=”,res1,res2)
_,res3:=getsumandsub(1,2)
Fmt.println(“res3=”,res3)
}
以上就是函数调用机制。需要注意的是在调用一个函数时,会给该函数分配一个新的空间,编译器会通过自身的处理让这个新的空间和其他的占的空间区分开来。
因此在每个函数对应的栈中,数据是独立存在的,不会混淆。当一个函数调用完毕或执行完毕后,程序会销毁这个函数对应的栈空间。