7.Go函数
1:函数定义
我想问一下大家,在大家小时候有没有玩过超级玛丽这个游戏?有同学说玩过,这确实是一款非常经典的游戏。
那么接下来我们模拟一下这个游戏的过程:
通过观察上面的代码,发现很多的代码是重复的,我们在编程中将这些重复的代码称为冗余代码,这种冗余代码带来的问题是,当我们的需求发生了变化后,需要进行多次的修改,这是一件非常痛苦的事情。那么我们应该怎样解决这个问题呢?就是用我们今天讲的函数。
(1.1)什么是函数呢?
函数就是将一堆代码进行重用的一种机制。函数就是一段代码,一个函数就像一个专门做这件事的人,我们调用它来做一些事情,它可能需要我们提供一些数据给它,它执行完成后可能会有一些执行结果给我们。要求的数据就叫参数,返回的执行结果就是返回值。
(1.2) 函数基本语法
func 函数名(){
函数体
}
通过func关键字来定义函数,函数名后面必须加括号。
接下来我们用函数改造上面的代码。
package main
import "fmt"
func PlayGame() {
fmt.Println("超级玛丽走呀走,跳呀跳,顶呀顶")
fmt.Println("超级玛丽走呀走,跳呀跳,顶呀顶")
fmt.Println("超级玛丽走呀走,跳呀跳,顶呀顶")
fmt.Println("超级玛丽走呀走,跳呀跳,顶呀顶")
fmt.Println("超级玛丽走呀走,跳呀跳,顶呀顶")
}
func WuDi() {
fmt.Println("屏幕开始闪烁")
fmt.Println("播放无敌的背景音乐")
fmt.Println("屏幕停止")
}
func main() {
PlayGame()
WuDi()
PlayGame()
WuDi()
}
执行如下:
注意:函数要执行必须调用,调用的方式通过函数名进行调用,但是千万不能忘记括号。
以上代码的执行流程是:先执行main()函数,前面我们讲过,main函数是整个程序的入口,所以我们一般将需要调用的函数名称写在main函数中。当执行到PlayGame()时,就会去执行PlayGame()函数体中的代码,该函数体代码执行完毕后,又回到main()函数,继续往下执行,这时执行到WuDi(),就去执行WuDi()函数体中的代码,执行完后又回到main()函数,以此类推,指导将main()函数中的所有代码执行完毕为止。
通过上面我们定义的函数,其实我们也能够发现一个规律就是:我们可以在我们的程序中定义多个函数,但是一般都会将相同要求,相同功能的代码放在一个函数中,将另外功能,另外要求的的代码放在另外一个函数中(这样,结构特别清晰,看一下函数名或者注释就知道该函数实现什么功能),也就是基本上每一个函数都是实现单独的功能(例如:求和的函数,就完成求和的功能,不要在改函数中又去完成计算“闰年”,如果要完成计算“闰年”,再定义一个函数。函数的功能一定要单一),这也是定义函数的基本原则。
另外,通过上面的案例我们也发现函数确实解决了我们一开始提出的问题,就是当需求发生了变化的时候,修改起来非常方便。例如,当WuDi()函数发生了需求的变化,我们只需要修改该函数就可以,那么调用该函数的地方,都发生了修改,也就是只修改了一次。不像以前,要进行多次的修改。
上面的函数是我们自己定义的(一般我们称为自定义函数),但是我们也已经学过不少GO语言自己定义的函数。例如:
fmt.Println("hello world")
fmt.Scanf("%d",&d)
main()
等。我们发现有些函数在使用的时候,必须给它提供一些数据,例如Println()函数和Scanf()函数。我们将给函数提供的这些数据称为参数,那么我们自己定义的函数是否可以有参数呢?完全可以,下面我们就看一下怎样给自己定义的函数提供参数。
给函数传递参数分为两种情况:第一种情况为普通参数列表,第二种情况为不定参数列表。
我们先讲解普通参数列表
2:普通参数列表
所谓的普通参数列表指的是,我们给函数传递的参数的个数都是确定好。基本语法如下:
func Test(a int, b int){
fmt.Printf("a=%d, b=%d", a, b)
}
func main() {
Test(3, 5)
}
首先我们定义了一个Test()函数,该函数有两个参数,a,和b .并且这两个参数的类型都是整型的(这两个参数我们称之为形参),在调用Test()函数时,我们将3传递给参数a,将5传递给参数b(在调用时输入的3和5这个参数我们称之为实参)。我们把这个过程称为参数的传递,并且在Test()函数中输出两个变量的值。
什么时候传递参数呢?其实就是根据我们的需求,例如:定义一个函数,专门实现两个数的和。
func SumAdd(a int, b int) {
var sum int
sum = a + b
fmt.Println("和为: ", sum)
}
func main() {
var num1, num2 int
fmt.Println("请输入第一个数: ")
fmt.Scanf("%d\n", &num1)
fmt.Println("请输入第二个数:")
fmt.Scanf("%d", &num2)
SumAdd(num1, num2)
}
执行如下:
根据上面的案例我们总结出,参数的个数和类型可以根据需要去确定。
但是一定要注意:在定义函数时,形参与实参的个数与类型都要保持一致。
如下所示:
形参是两个参数,但是实参确只传递了一个参数,在编译的时候会出错。
同理,形参参数只定义了一个,实参传递了两个,也会出错。
SumAdd函数需要的两个参数的类型都是整型的,所以该函数的参数也可以写成如下的形式:
难么这时候参数a的类型是整型。但是,不建议这样定义,因为不够清晰。
请看如下方法的定义:
func MyFunc05(a, b string, c float64, d, e int) {
}
该方法的参数类型分别什么?
如果我们将上面的方法定义成如下形式,大家看一下是不是非常清晰。
func MyFunc06(a string, b string, c float64, d int, e int) {
}
3:不定参数列表
根据前面的讲解,我们都知道了,在定义函数的时候根据需求指定参数的个数和类型,但是有时候如果无法确定参数的个数呢?
举例说明:上一小节我们写过一个求两个整数之和的函数,但是在实际的开发中,也会经常遇到这样的情况,就是项目经理(对整个项目的进度进行把控,对程序员进行管理的人员,称为项目经理),要求你写一个函数,实现整数的和。在这个要求中,项目经理并没有说清楚到底是有几个整数,那么我们应该怎样确定该函数的参数呢?就用接下来给大家讲解的“不定参数列表”来解决这个问题
那么我们可以通过如下的方式来定义函数:
func Test(args ...int) {
for i := 0; i < len(args); i++ {
fmt.Print(args[i])
}
fmt.Println("")
}
func main() {
Test(1)
Test(1, 2)
Test(1, 2, 3)
}
Test()函数的参数名字叫 args(参数的名字可以随便起),类型是整型的。但是,大家一定要注意,在args后面跟了三个点,就是表示该参数可以接收0或多个整数值。所以,args这个参数我们可以想象成是一个集合(类似数学中集合),可以存放多个值。
所以,在Test()函数内,我们通过计算长度函数叫len(),来计算出args这个集合中存储了多少个数(如果args这个集合中存储了5个数,那么len()函数的值就是5),通过for循环将该集合中的数全部输出,在输出时我们通过下标的方式将args集合中的值输出的。所谓的下标,我们可以理解成就是一个编号,对存储在args这个集合中每个数字都加上了编号。在这里要注意的是:下标是从0开始计算的。如下图所示:
args集合中存储了5,6,7三个数,对应的下标(编号),分别是0,1,2. 如果该集合中存储了4个数,那么第4个数的编号就是3.
现在取出第一个数就是args[0]值为5,第二个数args[1]值为6,以此类推。
在main()函数中,我们分别调用了三次Test()函数,在第一次调用时,我们只传递了一个参数1,那么形参args中也就只有1,所以只循环了一次就将该值输出。第二次调用时,传递了两个参数,循环两次输出,第三次调用,传递了三个参数,循环了三次输出。
在Test()函数中,我们除了使用len()函数,计算出集合中存储的数据的个数,然后再输出以外,还有另外一种输出的方式就是使用range关键字。如下所示:
func Test2(args ...int) {
for i, data := range args {
fmt.Printf("编号为: %d, 值为: %d\n", i, data)
}
}
func main() {
Test2(1, 2, 3)
}
执行如下:
range会从集合中返回两个数,第一个是对应的坐标,赋值给了变量i,第二个就是对应的值,赋值了变量data
所以以上两种输出集合的方式,大家在以后的开发过程中都可以使用。
当然在使用不定参数时,要注意几个问题:
第一:一定(只能)放在形参中的最后一个参数。例如:
func Test2(a int,args ...int) { // 不定参数 args ...int 只能放在形参的最后一位
for i, data := range args {
fmt.Printf("编号为: %d, 值为: %d\n", i, data)
}
fmt.Println("a=", a)
}
上面我们定义了一个Test2()函数,该函数第一个参数是一个普通的整型类型,第二个参数是不定参数。那么不定参数args,必须放在后面,整型类型的参数a必须放在前面.如果两者的位置进行互换,如下所示:
在编译的时候就会出错,出现的错误信息如下:
可能有同学会问,如果我现在根据需求,定义一个函数能够确定出两个具体的参数,类型是整型的,但是无法确定出其它参数的个数,那么该函数在定义的时候,是否是将两个能确定的整型参数放在前面,不定参数放在最后呢?是的,如下所示:
只要大家记住,不定参数一定要放在最后。
第二:在对函数进行调用时,固定参数必须传值,不定参数可以根据需要来决定是否要传值。
示例如下:
func Test2(a int,b int, args ...int) { // 不定参数 args ...int 只能放在形参的最后一位
for i, data := range args {
fmt.Printf("编号为: %d, 值为: %d\n", i, data)
}
fmt.Println("a=", a)
fmt.Println("b=", b)
}
func main() {
Test2(1, 2, 3,4,5)
}
我们定义了一个Test2()函数,该函数需要两个固定参数,和不定参数,在main()函数中,我们对Test函数进行了调用,实参1,2这两个值分别传递给形参中的a和b,实参中的3,5,6传递给了不定参数。
但是如果我们在调用Test2()函数时,一个参数都不传递,那么会出错。例如:
出现的错误信息如下:
但是,如果Test2()函数只有不定参数,没有固定参数,那么在调用时,可以根据需要来决定是否进行传值。例如:
我们在调用Test1()函数时,没有传递任何的参数,程序并没有出现任何的错误,当然也没有输出结果。
最后我们来实现一下,一开始提出的“写一个函数,实现整数的和”这个问题,这个函数现在实现起来就非常简单了。
func Sum(args ...int){
var sum int
for _,data := range args{
sum += data
}
fmt.Println("所有整数的和: ", sum)
}
func main() {
Sum(3,5,9)
}
在以上的案例中,我们用到了一个下划线( _ ),该下划线表示匿名变量,丢弃数据不进行处理,也就是任何赋予它的值都会被丢弃。
在前面讲解的时候,我们只是说过匿名变量的语法,没有讲解其应用的场景,那么大家可以通过该案例体会出匿名变量的应用场景。
4:函数嵌套调用
(4.1)基本函数嵌套调用
函数也可以像我们在前面学习if选择结构,for循环结构一样进行嵌套使用。所谓函数的嵌套使用,其实就是在一个函数中调用另外的函数。
func TestA(num1 int, num2 int) {
fmt.Println("num1 + num2 = ", num1+num2)
}
func TestB(a int, b int) {
TestA(a, b) // TestB 调用 TestA
}
func main() {
TestB(3,5) // 调用 TestB
}
执行如下:
num1 + num2 = 8
函数嵌套执行的过程:
(1) 先执行main()函数,在main()函数中调用TestB()函数,同时将参数分别传递给TestB()函数的a,b
(2) TestB( )函数中调用TestA( )函数,进行参数的传递。
(3) 执行TestA( )函数中的代码,打印两个数的和。
(4) TestA( )函数中所有的代码执行完成后,会回到TestB( )函数,执行TestB( )函数剩余的代码。
(5) 当TestB( )函数中所有的代码执行完成后,会回到main( )函数,执行main( )函数后面剩余的代码。
思考:根据以上函数嵌套执行的流程分析,下面函数的执行结果是:
func TestA(num1 int, num2 int) {
fmt.Println("num1 + num2 = ", num1+num2)
fmt.Println("TestA")
}
func TestB(a int, b int) {
TestA(a, b) // TestB 调用 TestA
fmt.Println("TestB")
}
func main() {
TestB(3,5) // 调用 TestB
fmt.Println("main")
}
执行如下:
num1 + num2 = 8
TestA
TestB
main
函数的嵌套调用在以后的开发中应用场景有很多,例如:大家都有在网站注册信息的经历,下面我们模拟一下这个注册过程,让大家体会一下函数嵌套调用在实际开发中的应用场景。等大家升到就业班后,会给大家讲解怎样开发网站。
一般的注册过程如下:在网页上填写信息,信息填写完成后,点击注册按钮,如果信息填写全部正确,会提示注册成功,并给您的邮箱发送信息,如果您填写的信息有错误(例如:邮件格式不正确),会提示相应的错误信息,不会注册成功,也不会发邮件。
根据以上信息,大家思考一下,我们可以定义几个函数?
前面我们在讲解函数定义的时候说过,定义函数的基本原则是:基本上每一个函数都是实现单独的功能。所以我们可以定义如下几个函数:Register( )注册函数,作用是接收用户在网页上填写的信息,完成用户信息的保存(只有将信息存储起来,等你下次登录时,将你填写的用户名和密码与注册时保存的用户名和密码进行比较)。CheckInfo( )函数,该函数的作用是对用户填写的信息进行校验,SendMsg( )函数完成邮件的发送。
那么这三个函数之间的调用关系是怎样的?
示例如下所示:
package main
import "fmt"
//发送邮件信息
func SendMsg() {
}
// 完成用户的校验
func CheckInfo(userName string, userPwd string, userEmail string) {
}
// 注册用户信息
func Register() {
// 接收用户在网页中填写的信息
// 对用户信息校验
CheckInfo("lijw", "123", "lijw@163.com")
// 完成用户信息保存
// 发送电子邮件
fmt.Println("用户注册成功")
}
func main() {
Register()
}
通过以上案例,希望大家仔细体会嵌套函数的应用场景。而且,我也相信大家对函数的好处有了很深入的理解(例如:用户登录成功,如果也要发送邮件,可以直接调用SendMsg()函数)。
(4.2) 不定参数函数调用
不定参数的函数在调用的时候,要注意一些细节问题。
我们通过案例给大家演示一下:
func TestC(args ...int) {
for _,data := range args{
fmt.Println(data)
}
}
func TestD(args ...int) {
TestC(args...) // 将不定参数 传递 TestC
}
func main() {
TestD(3,5,6)
}
执行如下:
3
5
6
TestC(args...)表示将参数全部传递,所以TestD( )函数最终的输出结果是:3,5,9
如果我们只想传递一部分数据,而不是传递所有的数据,应该怎样进行传递呢?
func TestC(args ...int) {
for _,data := range args{
fmt.Println(data)
}
}
func TestD(args ...int) {
TestC(args[2:]...) // 将不定参数 传递 TestC
}
func main() {
TestD(3,5,6)
}
另外一种写法:
以上程序的输出结果是3, 5
5:函数返回值
(5.1) 返回值函数基本定义
前面我们学习过一个GO自带的函数,len( )函数。该函数的作用是获取集合中数据的个数,也就是说该函数有返回值。
我们拿到该返回值后,就可以做进一步的处理,例如:可以用来作为循环条件。
我们自己定义的函数怎样返回值呢?
基本语法如下:
package main
import "fmt"
func TestSum() int { // int 表示该函数返回整型数据
var num1 int = 5
var num2 int = 5
var sum int
sum = num1 + num2
return sum // 返回sum变量
}
func main() {
var result int
result = TestSum() // 接收返回值
fmt.Println(result)
}
(1) 在定义函数TestSum( )时,后面加了int,表示该函数最终返回的是一个整型的数据
(2) 在TestSum( )函数中要返回数据,必须要返回的数据放在return关键字之后(通过return关键字返回数据)。
(3) 在main( )中调用TestSum( )函数,这时会执行TestSum( )函数中的代码,当执行完 return sum时,会将sum变量中保存的值返回。
(4) TestSum( )函数返回的值会赋值给main( )函数中的result变量。
以上是定义一个具有返回值函数的基本语法,当然,GO语言也提供了另外一种语法定义具有返回值的函数,如下所示:
第三种写法:
以上几种写法,都可以大家可以根据自己的习惯进行选择。
以上案例中,没有给TestSum( )函数传递参数,如果需要对TestSum( )函数进行参数传递,可以按照前面讲解的参数传递的内容,对函数进行参数进行传递。
案例演示如下:
(5.2)返回多个值
上面案例中,我们定义的函数都是返回一个指,那么一个函数是否可以返回多个值呢?可以,具体语法如下:
package main
import "fmt"
func TestParams() (a, b, c int) {
a, b, c = 1, 2, 3
return a, b, c // 返回多个值
}
func main() {
var result1 int
var result2 int
var result3 int
// 接收多个返回值
result1, result2, result3 = TestParams()
fmt.Printf("result1=%d, result2=%d, result3=%d", result1, result2, result3)
}
执行如下:
第二种写法:
函数的返回值,在实际的开发中应用也是非常广泛的,下面我们还是以前面讲的“用户注册”,这个案例说一下:
在用户注册这个案例中,我们定义了一个函数Register( )函数完成用户信息的接收和保存,但是在保存之前调用了CheckInfo()函数来校验接收到的用户信息。但是我们前面写的案例中,有一个问题就是如果没有通过检验,是不允许保存的,但是前面的案例中并没有对这种情况进行判断,所以案例修改成如下所示:
package main
import "fmt"
//发送邮件信息
func SendMsg() {
}
// 完成用户的校验
func CheckInfo(userName string, userPwd string, userEmail string) (b bool) {
// 对传递过来的信息进行校验,如果全部正确返回true,否则返回false
if userName != "" && userPwd != "" && userEmail != "" {
b = true
}else {
b = false
}
return // 返回校验结果
}
// 注册用户信息
func Register() {
// 接收用户在网页中填写的信息
// 对用户信息校验
var flag bool = false
// 接收返回的校验结果,赋值给变量flag
flag = CheckInfo("lijw", "123", "lijw@163.com")
// 校验结果成功,则发送邮件;反之,提示注册失败
if flag {
// 完成用户信息保存
// 发送电子邮件
SendMsg()
fmt.Println("用户注册成功")
}else {
fmt.Println("用户注册失败")
}
}
func main() {
Register()
}
通过以上案例,希望大家对函数的返回值在实际开发应用中,有深入的体会。
6:函数类型
在讲解函数类型之前,我们先简单的回顾一下,前面我们是怎样定义一个函数,以及怎样调用一个函数的。
我们通过如下的案例简单复习一下:
package main
import "fmt"
func Add(a int, b int) (sum int) {
sum = a + b
return
}
func main() {
var s int
s = Add(3, 6)
fmt.Println(s)
}
通过上面的案例,我们将函数的定义,参数传递,返回值都复习了一下。
在GO语言中还有另外一种定义使用函数的方式,就是函数类型,所谓的函数类型就是将函数作为一种类型可以用来定义变量,这种用法类似于前面我们讲过的int ,float64,string等类型,这些类型都是可以用来定义变量。
函数类型基本语法如下:
package main
import "fmt"
func Add(a int, b int) (sum int) {
sum = a + b
return
}
// 定义函数类型
type FuncType func(a int, b int) int
func main() {
var s int
//s = Add(3, 6)
//fmt.Println(s)
var result FuncType // 定义函数类型的变量
result = Add // 将Add函数 赋值给 result
s = result(3,6) // 使用 result 来计算数据
fmt.Println(s)
}
说明如下:
-
type 关键字后面跟着类型的名字(FunType),FunType就是一个类型.那么FunType是一个什么类型呢?
-
是一个函数类型,因为FunType后面跟着func(用来定义函数的),但是这里注意的是没有函数名字。
-
那么FunType是怎样的一个函数类型呢?是一个需要传递两个整型参数,有一个整型返回值的函数类型。
既然函数类型类似于我们前面学习过的 int ,string 等类型,那么函数类型可以用来定义变量。
var result FuncType // 表示定义了一个变量叫result,该变量的 类型是FuncType类型,而该类型是一个函数类型。
下面我们可以使用result这个函数类型的变量来调用函数了。
result = Add // 将Add函数 赋值给 result,将要调用的函数的名字赋值给result变量(也可以理解成将result变量指向了要调用的函数)
这里要注意的是:
-
第一: Add后面不能加括号。
-
第二:函数类型变量result要和将要调用的函数Add保持一致,所谓一致就是我们定义的函数类型FuncType的变量result,只能调用参数是两个整型的,并且有一个返回值,而且也是整型的函数。那么Add函数完全满足要求。
现在已经完成了函数类型变量result指向了函数Add,那么我们可以使用函数类型的变量result调用函数:
s = result(3,6) // 使用 result 来计算数据, 完成函数的调用
这是我们在GO语言中使用函数类型的方式完成函数的调用,可能有同学感觉这种方式比较麻烦,不如我们前面讲解的方式使用起来简单。那么在这里,要求大家先理解这种方式,能记住语法就可以了。关于该方式的优势,等我们讲解完面向对象编程这个知识点后,大家就有深刻的理解了。
7:函数作用域
(7.1) 局部变量
前面我们定义的函数中,都经常使用变量。那么我们看一下如下程序的输出结果:
package main
import "fmt"
func TestE(){
a := 5
a += 1
}
func main() {
a := 9
TestE()
fmt.Println(a)
}
执行如下:
最终的输出结果是9,为什么呢?
在执行fmt.Println(a)语句之前,我们已经调用了函数TestE(),并在该函数中我们已经重新给变量a赋值了。但是为什么结果没有发生变化呢?
这就是变量的作用范围(作用域)的问题。
在TestE( )函数中定义的变量a,它的作用范围只在改函数中有效,当TestE( )函数执行完成后,在该函数中定义的变量也就无效了。也就是说,当TestE( )函数执行完以后,定义在改函数中所有的变量,所占有的内存空间都会被回收。
所以,我们把定义在函数内部的变量称为局部变量。
局部变量的作用,为了临时保存数据需要在函数中定义变量来进行存储,这就是它的作用。
并且,通过上面的案例我们发现:不同的函数,可以定义相同的名字的局部变量,但是各用个的不会产生影响。例如:我们在main( )函数中定义变量a,在TestE( )函数中也定义了变量a,但是两者之间互不影响,就是因为它们属于不同的函数,作用范围不一样,在内存中是两个存储区域。
(7.2)全局变量
有局部变量,那么就有全局变量。
所谓的全局变量:既能在一个函数中使用,也能在其他的函数中使用,这样的变量就是全局变量.也就是定义在函数外部的变量就是全局变量。全局变量在任何的地方都可以使用。
案例如下:
package main
import "fmt"
var a int // 定义全局变量
func TestE(){
a = 5
a += 1
}
func main() {
a = 9
TestE()
fmt.Println(a)
}
执行如下:
注意:在上面的案例中,我们在函数外面定义了变量a,那么该变量就是全局变量,并且TestE( )函数和main( )函数都可以使用该变量。
该程序的执行流程是:先执行main( )函数,给变量a赋值为9,紧接着调用TestE( )函数,在改函数中完成对变量a的修改。
由于main( )函数与TestE( )函数所使用的变量a是同一个,所以当TestE( )函数执行完成后,变量的a已经变成了6. 回到main( )函数执行后面的代码,也就是 fmt.Println(a),输出的值就是6.
可能有同学已经发现该程序和我们前面写的程序还有一点不同的地方是:
第一个程序我们是a:=9,但是第二个程序执行修改成了 a=9, 现在修改一下第二个程序如下:
该程序与上面的程序不同之处在于,该程序是a:=9
,上面的程序是a=9
.
现在大家思考一下该程序的结果是多少?
最终结果是9.
原因是:a:=9
等价于
var a int
a=9
也就是定义一个整型变量a,并且赋值为9.
那么现在的问题是,我们定义了一个全局变量a,同时在main( )中又定义了一个变量也叫a,但是该变量是一个局部变量。
当全局变量与局部变量名称一致时,局部变量的优先级要高于全局变量。所以在main( )函数中执行fmt.Println(a)时输出的是局部变量a的值。但是TestE( )函数中的变量a还是全局变量。
注意:大家以后在开发中,尽量不要让全局变量的名字与局部变量的名字一样。
所以大家,思考以下程序执行的结果:
总结:
(1)在函数外边定义的变量叫做全局变量。
(2)全局变量能够在所有的函数中进行访问
(3)如果全局变量的名字和局部变量的名字相同,那么使用的是局部变量的,小技巧强龙不压地头蛇
8:匿名函数与闭包
(8.1)匿名函数
前面我们定义函数的时候,发现是不能在一个函数中,再次定义一个函数。如果我们想在一个函数中再定义一个函数,那么可以使用匿名函数,所谓匿名函数就是没有名字的函数。
如下所示:
package main
import "fmt"
func main() {
var num int
num = 9
// 匿名函数,没有函数名字
f := func() {
num++
fmt.Println("匿名函数: ", num)
}
f()
fmt.Println("main函数:", num)
}
执行如下:
匿名函数: 10
main函数: 10
在main( )函数中定义了一个匿名函数,定义的方式非常简单 func(){ 函数体 }, 一定要注意的是在func的后面没有函数的名字,同时在这里定义的该匿名函数也没有参数。我们将定义好的匿名函数赋值给了变量f,那么变量f就是一个函数类型。要想执行该匿名函数,就可以通过 f( )的方式去调用执行。
在这里,有一件非常有意思的事情,就是在匿名函数中可以直接访问main( )函数中定义的局部变量,并且在匿名函数中对变量的值进行了修改,最终会影响到整个main( )函数中定义的变量的值。所以上面两行输入都是10.
关于这一点,一定与上一节讲解的函数作用域进行区别。
匿名函数还有其它调用方式如下:
package main
import "fmt"
func main() {
var num int
num = 9
// 匿名函数,没有函数名字
f := func() {
num++
fmt.Println("匿名函数: ", num)
}
type FuncType func() // 函数没有参数,没有返回值
var f1 FuncType // 声明函数类型的变量
f1 = f // 将匿名函数赋值给函数类型变量
f1() // 执行函数
fmt.Println("main函数:", num)
}
上面案例中,定义的匿名函数赋值给了变量f,那么f的类型就是函数类型,所以我们自己也可以定义一个函数类型的变量来调用匿名函数。但是上面的应用比较繁琐,实际用的比较少。
定义匿名函数时,直接调用
package main
import "fmt"
func main() {
var num int
num = 9
// 匿名函数,没有函数名字
func() {
num++
fmt.Println("匿名函数: ", num)
}() // 使用括号() 直接调用匿名函数
fmt.Println("main函数:", num)
}
该方式,需要在匿名函数的末尾加上小括号,表示调用。同时也不需要将定义好的匿名函数赋值给某个变量。
下面看一下怎样给匿名函数传递参数:
package main
import "fmt"
func main() {
// 匿名函数,带参数
func(a, b int) {
var sum int
sum = a + b
fmt.Println("和为: ", sum)
}(3, 6) // 调用时进行参数传递
}
或者如下方式:
func main() {
// 匿名函数,带参数
f := func(a, b int) {
var sum int
sum = a + b
fmt.Println("和为: ", sum)
}
f(3, 6)
}
匿名函数如果有返回值,怎样进行处理呢?
// 匿名函数,有参有返回值
x, y := func(i, j int) (max, min int) {
if i > j {
max = i
min = j
} else {
max = j
min = i
}
return
}(10, 20)
fmt.Printf("x = %d, y = %d\n", x, y)
以上案例中定义了一个匿名函数,该匿名函数需要两个整型参数,同时指定了该函数返回值的名字是变量max与min
当执行到return时,让变量max与min返回,赋值了变量x,y。x中存储的是max变量的值,y中存储的是min变量的值。
以上就是关于什么是匿名函数,以及匿名函数的使用。匿名函数最主要的功能就是实现了闭包。
(8.2)闭包
所谓的闭包是指有权访问另一个函数作用域中的变量的函数,就是在一个函数内部创建另一个函数。
在Go语言里,所有的匿名函数(Go语言规范中称之为函数字面量)都是闭包。
根据以上定义,那么上一节定义的匿名函数其实就是闭包。(仔细体会上一节定义的匿名函数)
(也可以这样理解闭包:虽然不能在一个函数里直接声明另一个函数,但是可以在一个函数中声明一个函数类型的变量,此时的函数称为闭包(closure))
下面我们通过一个案例,看一下关于闭包的应用。
思考:以下程序执行的结果是:
package main
import "fmt"
func TestX() int {
var x int
x++
return x
}
func main() {
fmt.Println(TestX())
fmt.Println(TestX())
fmt.Println(TestX())
}
执行如下:
1
1
1
虽然TestX( )函数调用了三次,但是输出都是1.
原因是:
每次调用TestX( )函数,都是重新声明变量x,当函数执行完成后,x会自动被释放所占资源。
如果想实现累加运算,这里就需要用到闭包(匿名函数)。
func TestX() func() int { // 定义匿名函数类型作为返回参数 func()
var x int
return func() int{
x++
return x
}
}
func main() {
f := TestX()
fmt.Println(f())
fmt.Println(f())
fmt.Println(f())
}
执行如下:
1
2
3
由于在定义TestX( )函数时指定了返回的类型是一个匿名函数,并且该匿名函数返回的类型是整型。
所以在TestX( )函数中定义了一个匿名函数,并且将整个匿名函数返回,匿名函数返回的是整型。
在main( )函数中定义了一个变量f,该变量的类型是匿名函数,f( )表示调用执行匿名函数。
最终执行完成后发现,实现了数字的累加。
因为匿名函数(闭包),有一个很重要的特点:
它不关心这些捕获了的变量和常量是否已经超出了作用域,所以只有闭包还在使用它,这些变量就还会存在。
9:递归函数
通过前面的学习知道一个函数可以调用其他函数。
如果一个函数在内部不调用其它的函数,而是自己本身的话,这个函数就是递归函数。
例如:
package main
import "fmt"
func FuncTest(a int) {
// 当a=1的时候,返回
if a == 1 {
fmt.Println("a=", a)
return
}
// 自己调用自己
FuncTest(a - 1)
fmt.Println("abc=", a)
}
func main() {
FuncTest(3)
}
执行如下:
注意:递归函数的执行流程。首先递归函数需要设定一个边界值返回,在自己调用自己的时候,将传递的值往边界值的方向靠拢,最后逐步返回打印结果。
递归函数的作用:
举个例子,我们来计算阶乘 n! = 1 * 2 * 3 * ... * n
package main
import "fmt"
// 举个例子,我们来计算阶乘 n! = 1 * 2 * 3 * ... * n
func Func(n int) int {
// 当n=1的时候,返回 1! = 1
if n == 1 {
return 1
} else {
// 自己调用自己
return Func(n-1) * n // 1*2*3....*n
}
}
func main() {
sum := Func(3) // 1*2*3=6
fmt.Println("sum = ", sum)
}
执行如下:
其它应用场景:
电商网站中的商品类别菜单的应用。
查找某个磁盘下的文件。