方法: 完成一个功能的代码或集合。
大致过程:给方法必要的输入,方法返回结果。
函数调用机制的底层分析:
栈区:基本数据类型一般分配到栈区,编译器存在一个逃逸分析。
堆区:引用数据类型一般分配到堆区,编译器存在一个逃逸分析。
代码区:存放代码的内存区域
通过一个调用过程,深刻理解函数调用都做了什么:
package main
import "fmt"
func calcu (a float64,b float64) (float64,float64) {
plus := a + b
minus := a - b
return plus,minus
}
func main() {
/*
第一步 main是一切程序的入口,首先现在内存栈区开辟一块空间存储变量
a=5 b=3 都是基本变量,一般存在内存栈区
*/
var (
a float64 = 5
b float64 = 3
)
/*
第二步 调用函数,将两个变量的值传入函数中,系统在内存栈区开辟另一块儿空间放入被调用函数,
这个地方的函数的内存与main程序的内存空间完全是两个用户空间,这也就是说为何被调用函数如果
没有返回值,就是两个完全不相干的函数调用,也就失去了调用意义,调用结束返回后,return结束
被调用函数,并且返回值,编译器就会将这部分垃圾回收,回到main()中继续走完
*/
max, min := calcu(a,b)
/*
第三部 执行接下来main中的其他语句,直到执行完成后,编译器回收所有占据内存的垃圾。
*/
fmt.Println(max)
fmt.Println(min)
}
函数的递归调用
- 一个函数在函数体中本身又调用了自身,就是递归调用。
- 案例:
package main
import “fmt”
func recu (n float64) {
if n > 2 {
n–
recu(n) //recu里面迭代recu(),每次迭代成功的部分都有独立的用户空间,变量彼此互不影响。
} else {
fmt.Println(“n =”,n)
} //直到函数结尾没有任何返回值,所以没有任何新值产生
}
func main() {
var a float64 = 4
recu(a) //没有得到任何返回值,所以接下来调用后也不会产生任何输出
}
-
递归永远要向终止递归的条件无限接近,否则就是无限递归,死循环。
-
当一个函数执行完毕,或者遇到return,就会返回,遵守谁调用返回给谁,同时当函数执行完毕或者返回时,该函数本身也会被系统销毁,不再占内存空间。
-
递归调用练习题:使用递归方式求出斐波那契数列,随机给第n个整数,求数列的值是多少?
package main import "fmt" func fbnq(n int) int { if n == 1 || n==2 { return 1 } else { return fbnq(n-1) + fbnq(n-2) } } func main() { res := fbnq(6) fmt.Println(res) }
-
求函数值,已知f(1)=3,f(n) = 2 * f(n) + 1, 当给一个n赋值,求出f(n)应该是多少?
package main import "fmt" func f(n int) int { if n == 1 { return 3 } else { return 2*f(n-1) + 1 } } func main() { res := f(6) fmt.Println(res) }
-
猴子吃桃子,第一天吃桃子总数的一半,再多吃一个,第十天发现只剩下一个桃子,求总共有多少个桃子
package main import ( "fmt" ) func f(n int) int { if n == 10 { return 1 } else { return (f(n+1) + 1) * 2 //其实这个else有问题,需要限定范围,1<n<10 } } func main() { res := f(1) fmt.Println(res) }
函数注意事项和细节讨论
8. 形参列表可以是多个,返回值列表也可以是多个,多个返回值的话必须加括号。
9. 形参列表和返回值列表数据类型可以是值类型也可以是引用类型。
10. 函数命名首字母不能是数字,因为首字母大小写决定函数可否被其他包调用,所以就好理解了,大写类似java的public,小写是private。
11. 函数的作用域:函数变量是局部的,函数外不生效,一个函数相当于一个用户空间。
12. 基本数据类型和数组默认都是值传递,在函数内修改,不会影响到原来的值。
或者理解为值拷贝,并不会影响调用函数本身的值。
13. 如果想通过函数内变量修改函数外的变量,可以传入变量的地址&,函数内以指针的方式操作变量,也就是说引用传递会影响main中的值。
**举例值传递,函数在变量+10后,并不会影响主函数中n值不变。**
```
package main
import "fmt"
func test(n int) int {
n = n + 10
fmt.Println("test n = ",n)
return n
}
func main() {
var n int = 10
test(n)
fmt.Println("main n = ",n)
} // main n=10
```
**举例指针传递,函数在指针取值后,影响到主函数中的n,使n+10变为了20:**
```
package main
import "fmt"
func test1(n *int) int {
*n = *n + 10
fmt.Println("test n = ",*n)
return *n
}
func main() {
var n int = 10
test1(&n)
fmt.Println("main n = ",n)
} //main n=20
```
-
Golang不支持传统的函数重载,不允许两个同名函数存在,即便形参个数不一样也不行。
-
Golang中函数也是一种数据类型,可以赋值给一个变量,该变量就是一个函数类型的变量,通过该变量可以对函数调用, 方便记忆可以理解 变量=函数 给函数加了个变量的别名,所以函数()没问题,那么 变量()也没问题。
-
函数也可以做调用函数的参数,
package main import "fmt" func getSum (n1 int,n2 int) int { return n1 + n2 } func ff (ff func(int,int) int, num1 int, num2 int) int { // ff类型为函数类型,与getSum一致,再传 n1 ,n2 形参, 再返回一个int返回值 return ff(num1 , num2) } func main() { res2 := ff(getSum,10,20) fmt.Println(res2) }
-
为了简化数据类型定义,Go支持自定义数据类型: type myint int //相当于为int加了个别名,var xx myint(实际上就是int)
最好的写法是处理函数作为形参,type myfunc func(int,int) int //相当于用myfunc类型就双形参单return函数类型 -
支持对函数返回值命名:在func定义的时候就定义return值 func (n1 int,n2 int) (sum int,sub int) {…} //这样就直接定义了两个返回值,没有顺序之分了。 这样函数最后只需要return了,并不需要再添加返回什么。
-
两个以上返回值可以用 _ 忽略,我只想得到其中一个,不想要其他。
-
Go支持可变参数,例如 func getSum (args… int) (sum int) {…} // args… 七个bytes缺一不可,传入0~多个参数。
func getSum (n1 float64 , args… int) (sum int) {…} // 传入一个float64,传入1~多个int参数
args是一个slice,通过args[i]可以访问到各个值,理解为动态数组。案例:针对func getSum (args… int) (sum int) {…} 如何传入参数:
package main import "fmt" func get1Sum (n1 int, args... int) int { //args可以变成其他的字符,vars也可以,但是一般约定俗成,不要乱改。 sum := n1 //可变参数args一定要放在形参列表里的最后 for i := 0 ; i < len(args) ; i++ { sum += args[i] } return sum } func main() { res := get1Sum(1,2,3,4,5) fmt.Println(res) }