-
一.函数
1.1 函数定义
我想问一下大家,在大家小时候有没有玩过超级玛丽这个游戏?有同学说玩过,这确实是一款非常经典的游戏。
那么接下来我们模拟一下这个游戏的过程:
通过观察上面的代码,发现很多的代码是重复的,我们在编程中将这些重复的代码称为冗余代码,这种冗余代码带来的问题是,当我们的需求发生了变化后,需要进行多次的修改,这是一件非常痛苦的事情。那么我们应该怎样解决这个问题呢?就是用我们今天讲的函数。
(1)什么是函数
函数就是将一堆代码进行重用的一种机制。函数就是一段代码,一个函数就像一个专门做这件事的人,我们调用它来做一些事情,它可能需要我们提供一些数据给它,它执行完成后可能会有一些执行结果给我们。要求的数据就叫参数,返回的执行结果就是返回值。
(2)函数基本语法
func 函数名(函数参数列表) (函数返回值列表){
函数体
}
通过func关键字来定义函数,函数名后面必须加括号。
接下来我们用函数改造上面的代码。
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()函数。我们将给函数提供的这些数据称为参数,那么我们自己定义的函数是否可以有参数呢?完全可以,下面我们就看一下怎样给自己定义的函数提供参数。
给函数传递参数分为两种情况:第一种情况为普通参数列表,第二种情况为不定参数列表。
我们先讲解普通参数列表
1.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 Sum(a int,b int) { sum := 0 sum = a + b fmt.Println("和为:",sum) } func main() { var num1,num2 int fmt.Print("请输入第一个数:") fmt.Scanf("%d ",&num1) fmt.Print("请输入第二个数:") fmt.Scanf("%d",&num2) Sum(num1,num2) }
根据上面的案例我们总结出,参数的个数和类型可以根据需要去确定。
但是一定要注意:在定义函数时,形参与实参的个数与类型都要保持一致。
如下所示:
func Test(a int,b int) { fmt.Printf("a=%d,b=%d",a,b) } func main() { Test(3) }
形参是两个参数,但是实参确只传递了一个参数,在编译的时候会出错。
同理,形参参数只定义了一个,实参传递了两个,也会出错。
func Test(a int) { fmt.Printf("a=%d,b=%d",a,b) } func main() { Test(3,5) }
上面的Sum函数需要的两个参数的类型都是整型的,所以该函数的参数也可以写成如下的形式:
func Sum(a, b int) { sum := 0 sum = a + b fmt.Println("和为:",sum) }
那么这时候参数a的类型是整型。但是,不建议这样定义,因为不够清晰。
请看如下方法的定义:
func MyFunc(a, b string, c float64, d, e int) { }
该方法的参数类型分别什么?
如果我们将上面的方法定义成如下形式,大家看一下是不是非常清晰。
func MyFunc(a string, b string, c float64, d int, e int) { }
1.3 不定参数列表
根据前面的讲解,我们都知道了,在定义函数的时候根据需求指定参数的个数和类型,但是有时候如果无法确定参数的个数呢?
举例说明:上一小节我们写过一个求两个整数之和的函数,但是在实际的开发中,也会经常遇到这样的情况,就是项目经理(对整个项目的进度进行把控,对程序员进行管理的人员,称为项目经理),要求你写一个函数,实现整数的和。在这个要求中,项目经理并没有说清楚到底是有几个整数,那么我们应该怎样确定该函数的参数呢?就用接下来给大家讲解的“不定参数列表”来解决这个问题。
不定参数列表的定义格式:
func 函数名(参数名 ...参数数据类型) (函数返回值列表){
函数体
}
那么我们可以通过如下的方式来定义函数:
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 Test(args ...int) { for i,data := range args{ fmt.Println("下标为:",i) fmt.Println("值为:",data) } } func main() { Test(1,2,3) }
range会从集合中返回两个数,第一个是对应的坐标,赋值给了变量i,第二个就是对应的值,赋值了变量data
所以以上两种输出集合的方式,大家在以后的开发过程中都可以使用。
当然在使用不定参数时,要注意几个问题:
第一:一定(只能)放在形参中的最后一个参数。例如:
func Test(a int,args ...int) { /* for i := 0;i < len(args);i++{ fmt.Print(args[i]) } */ for i,data := range args{ fmt.Println("下标为:",i) fmt.Println("值为:",data) } fmt.Println(a)
上面我们定义了一个Test()函数,该函数第一个参数是一个普通的整型类型,第二个参数是不定参数。那么不定参数args,必须放在后面,整型类型的参数a必须放在前面.如果两者的位置进行互换,如下所示:
func Test(args ...int,a int) { /* for i := 0;i < len(args);i++{ fmt.Print(args[i]) } */ for i,data := range args{ fmt.Println("下标为:",i) fmt.Println("值为:",data) } fmt.Println(a) }
在编译的时候就会出错,出现的错误信息如下:
syntax error: cannot use ... with non-final parameter args
可能有同学会问,如果我现在根据需求,定义一个函数能够确定出两个具体的参数,类型是整型的,但是无法确定出其它参数的个数,那么该函数在定义的时候,是否是将两个能确定的整型参数放在前面,不定参数放在最后呢?是的,如下所示:
func Test(a int,b int,args ...int) { /* for i := 0;i < len(args);i++{ fmt.Print(args[i]) } */ for i,data := range args{ fmt.Println("下标为:",i) fmt.Println("值为:",data) } fmt.Println(a) fmt.Println(b) }
只要大家记住,不定参数一定要放在最后。
第二:在对函数进行调用时,固定参数必须传值,不定参数可以根据需要来决定是否要传值。
示例如下:
func Test(a int,b int,args ...int) { /* for i := 0;i < len(args);i++{ fmt.Print(args[i]) } */ for i,data := range args{ fmt.Println("下标为:",i) fmt.Println("值为:",data) } fmt.Println(a) fmt.Println(b) } func main() { Test(1,2,3,4,5) }
我们定义了一个Test()函数,该函数需要两个固定参数,和不定参数,在main()函数中,我们对Test函数进行了调用,实参1,2这两个值分别传递给形参中的a和b,实参中的3,4,5传递给了不定参数。
但是如果我们在调用Test()函数时,一个参数都不传递,那么会出错。例如:
func Test(a int,b int,args ...int) { /* for i := 0;i < len(args);i++{ fmt.Print(args[i]) } */ for i,data := range args{ fmt.Println("下标为:",i) fmt.Println("值为:",data) } fmt.Println(a) fmt.Println(b) } func main() { Test() }
出现的错误信息如下:
not enough arguments in call to Test have () want (int, int, ...int)
但是,如果Test()函数只有不定参数,没有固定参数,那么在调用时,可以根据需要来决定是否进行传值。例如:
func Test(args ...int) { /* for i := 0;i < len(args);i++{ fmt.Print(args[i]) } */ for i,data := range args{ fmt.Println("下标为:",i) fmt.Println("值为:",data) } } func main() { Test() }
我们在调用Test()函数时,没有传递任何的参数,程序并没有出现任何的错误,当然也没有输出结果。
最后我们来实现一下,一开始提出的“写一个函数,实现整数的和”这个问题,这个函数现在实现起来就非常简单了。
func Sum(args ...int) { sum := 0 for _,data := range args{ sum += data } fmt.Println("所有整数的和为:",sum) } func main() { Sum(1,2,3,4,5) }
在以上的案例中,我们用到了一个下划线( _ ),该下划线表示匿名变量,丢弃数据不进行处理,也就是任何赋予它的值都会被丢弃。
在前面讲解的时候,我们只是说过匿名变量的语法,没有讲解其应用的场景,那么大家可以通过该案例体会出匿名变量的应用场景。
1.4 函数嵌套调用
(1)基本函数嵌套调用
函数也可以像我们在前面学习if选择结构,for循环结构一样进行嵌套使用。所谓函数的嵌套使用,其实就是在一个函数中调用另外的函数。
func Test1(num1 int,num2 int) { fmt.Println(num1 + num2) } func Test(a int,b int) { Test1(a,b) } func main() { Test(4,6) }
函数嵌套执行的过程:
- 先执行main()函数,在main()函数中调用Test()函数,同时将参数分别传递给Test()函数的a,b
- Test( )函数中调用Test1( )函数,进行参数的传递。
- 执行Test1( )函数中的代码,打印两个数的和。
- Test1( )函数中所有的代码执行完成后,会回到Test( )函数,执行Test( )函数剩余的代码。
- 当Test( )函数中所有的代码执行完成后,会回到main( )函数,执行main( )函数后面剩余的代码。
思考:根据以上函数嵌套执行的流程分析,下面函数的执行结果是:
func Test1(num1 int,num2 int) { fmt.Println(num1 + num2) fmt.Println("Test1") } func Test(a int,b int) { Test1(a,b) fmt.Println("Test") } func main() { Test(4,6) fmt.Println("main") }
函数的嵌套调用在以后的开发中应用场景有很多,例如:大家都有在网站注册信息的经历,下面我们模拟一下这个注册过程,让大家体会一下函数嵌套调用在实际开发中的应用场景。
一般的注册过程如下:在网页上填写信息,信息填写完成后,点击注册按钮,如果信息填写全部正确,会提示注册成功,并给您的邮箱发送信息,如果您填写的信息有错误(例如:邮件格式不正确),会提示相应的错误信息,不会注册成功,也不会发邮件。
根据以上信息,大家思考一下,我们可以定义几个函数?
前面我们在讲解函数定义的时候说过,定义函数的基本原则是:基本上每一个函数都是实现单独的功能。所以我们可以定义如下几个函数:Register( )注册函数,作用是接收用户在网页上填写的信息,完成用户信息的保存(只有将信息存储起来,等你下次登录时,将你填写的用户名和密码与注册时保存的用户名和密码进行比较)。CheckInfo( )函数,该函数的作用是对用户填写的信息进行校验,SendMsg( )函数完成邮件的发送。
那么这三个函数之间的调用关系是怎样的?
示例如下所示:
//发送邮件信息 func SendMsg() { } //完成用户信息校验 func CheckInfo(userName string,userPwd string,userEmail string) { } //注册用户信息(接受用户信息,保存用户信息) func Register() { //接受用户在网页中填写的信息 //对用户信息校验 CheckInfo("admin","123456","abc@123.com") //完成用户信息保存 //发送电子邮件 SendMsg() fmt.Println("用户注册成功!!") } func main() { Register() }
通过以上案例,希望大家仔细体会嵌套函数的应用场景。而且,我也相信大家对函数的好处有了很深入的理解(例如:用户登录成功,如果也要发送邮件,可以直接调用SendMsg()函数)。
(2)不定参数函数调用
不定参数的函数在调用的时候,要注意一些细节问题。
我们通过案例给大家演示一下:
func Test1(args ...int) { for _,data := range args{ fmt.Println(data) } } func Test(args ...int) { Test1(args...) } func main() { Test(1,2,3) }
Test1(args...)表示将参数全部传递,所以Test1( )函数最终的输出结果是:1,2,3
如果我们只想传递一部分数据,而不是传递所有的数据,应该怎样进行传递呢?
func Test1(args ...int) { for _,data := range args{ fmt.Println(data) } } func Test(args ...int) { //将编号为2(包含2)以后的数据全部传递,不定参数的下标从0开始计算。 Test1(args[2:]...) } func main() { Test(1,2,3,4,5) }
以上案例的输出结果 :3,4,5
另外一种写法:
func Test1(args ...int) { for _,data := range args{ fmt.Println(data) } } func Test(args ...int) { //将编号0到编号2(不包含2)之间的数据全部传递。 Test1(args[:2]...) } func main() { Test(1,2,3,4,5) }
以上程序的输出结果是:1,2
1.5 函数返回值
(1)返回值函数基本定义
前面我们学习过一个GO自带的函数,len( )函数。该函数的作用是获取集合中数据的个数,也就是说该函数有返回值。
我们拿到该返回值后,就可以做进一步的处理,例如:可以用来作为循环条件。
我们自己定义的函数怎样返回值呢?
基本语法如下:
func Test() int { num1 := 4 num2 := 6 var sum int sum = num1 + num2 return sum } func main() { var result int result = Test() fmt.Println(result) }
- 在定义函数Test( )时,后面加了int,表示该函数最终返回的是一个整型的数据
- 在Test( )函数中要返回数据,必须要返回的数据放在return关键字之后(通过return关键字返回数据)。
- 在main( )中调用Test( )函数,这时会执行Test( )函数中的代码,当执行完 return sum时,会将sum变量中保存的值返回。
- Test( )函数返回的值会赋值给main( )函数中的result变量。
以上是定义一个具有返回值函数的基本语法,当然,GO语言也提供了另外一种语法定义具有返回值的函数,如下所示:
func Test() (sum int) { num1 := 4 num2 := 6 //var sum int sum = num1 + num2 return sum } func main() { var result int result = Test() fmt.Println(result) }
第三种写法:
func Test() (sum int) { num1 := 4 num2 := 6 //var sum int sum = num1 + num2 return } func main() { var result int result = Test() fmt.Println(result) }
以上几种写法,都可以大家可以根据自己的习惯进行选择。
以上案例中,没有给Test( )函数传递参数,如果需要对Test( )函数进行参数传递,可以按照前面讲解的参数传递的内容,对函数进行参数进行传递。
案例演示如下:
func Test(num1 int,num2 int) (sum int) { //var sum int sum = num1 + num2 return } func main() { var result int result = Test(4,6) fmt.Println(result) }
(2)多个返回值
上面案例中,我们定义的函数都是返回一个指,那么一个函数是否可以返回多个值呢?可以,具体语法如下:
func Test()(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 = Test() fmt.Printf("result1=%d,result2=%d,result3=%d ",result1,result2,result3) }
第二种语法:
func Test()(a,b,c int) { a,b,c = 1,2,3 return } func main() { var result1 int var result2 int var result3 int result1,result2,result3 = Test() fmt.Printf("result1=%d,result2=%d,result3=%d ",result1,result2,result3) }
函数的返回值,在实际的开发中应用也是非常广泛的,下面我们还是以前面讲的“用户注册”,这个案例说一下:
在用户注册这个案例中,我们定义了一个函数Register( )函数完成用户信息的接收和保存,但是在保存之前调用了CheckInfo()函数来校验接收到的用户信息。但是我们前面写的案例中,有一个问题就是如果没有通过检验,是不允许保存的,但是前面的案例中并没有对这种情况进行判断,所以案例修改成如下所示:
//发送邮件信息 func SendMsg() { } //完成用户信息校验 func CheckInfo(userName string,userPwd string,userEmail string) (status bool) { //对传递过来的信息进行校验,如果全部正确返回true,否则返回false if userName != "" && userPwd != "" && userEmail != ""{ status = true }else { status = false } return } //注册用户信息(接受用户信息,保存用户信息) func Register() { //接受用户在网页中填写的信息 //对用户信息校验 flag := CheckInfo("admin","123456","abc@123.com") if flag{ //完成用户信息保存 //发送电子邮件 SendMsg() fmt.Println("用户注册成功!!") }else { fmt.Println("用户注册失败") } } func main() { Register() }
通过以上案例,希望大家对函数的返回值在实际开发应用中,有深入的体会。
1.6 函数类型
在讲解函数类型之前,我们先简单的回顾一下,前面我们是怎样定义一个函数,以及怎样调用一个函数的。
我们通过如下的案例简单复习一下:
func Test(a int,b int) (sum int) { sum = a + b return sum } func main() { s := Test(3,5) fmt.Println(s) }
在GO语言中还有另外一种定义使用函数的方式,就是函数类型,所谓的函数类型就是将函数作为一种类型可以用来定义变量,这种用法类似于前面我们讲过的int ,float64,string等类型,这些类型都是可以用来定义变量。
先根据下面的例子看一下,函数类型是怎么表示的:
func Test1() { fmt.Println("helloworld") } func Test2(a int,b int) { fmt.Println(a+b) } func Test3(a int,b int)(string,bool) { return "",true } func main() { fmt.Printf("%T ",Test1) fmt.Printf("%T ",Test2) fmt.Printf("%T ",Test3) }
结果如下:
func() func(int, int) func(int, int) (string, bool)
这里我们可以发现函数类型是跟函数的形参和返回值有关。那么如何通过函数类型来定义使用函数呢,示例如下:
func Test(a int,b int) (sum int) { sum = a + b return sum } //定义函数类型 type FuncType func(int,int) int func main() { s := 0 //定义函数类型变量 var result FuncType //给函数类型变量赋值 result = Test s = result(4,6) fmt.Println(s) }
说明如下:type关键字后面跟着类型的名字(FuncType),FuncType就是一个类型,那么FuncType是一个什么类型呢?
是一个函数类型,因为FuncType后面跟着func(用来定义函数的),但是这里注意的是没有函数名字。那么FuncType是怎样的一个函数类型呢?是一个需要传递两个整型参数,有一个整型返回值的函数类型。
既然函数类型类似于我们前面学习过的 int ,string 等类型,那么函数类型可以用来定义变量。
var result FuncType //表示定义了一个变量叫result,该变量的类型是FuncType类型,而该类型是一个函数类型。
下面我们可以使用result这个函数类型的变量来调用函数了。
result=Test //将要调用的函数的名字赋值给result变量(也可以理解成将result变量指向了要调用的函数),这里要注意的是:第一:Test后面不能加括号;第二:函数类型变量result要和将要调用的函数Test保持一致,所谓一致就是我们定义的函数类型FuncType的变量result,只能调用参数是两个整型的,并且有一个返回值,而且也是整型的函数。那么Test函数完全满足要求。
现在已经完成了函数类型变量result指向了函数Test,那么我们可以使用函数类型的变量result调用函数:
result(3,6) //完成函数的调用。
另外还可以通过自动推导类型来创建函数类型变量
示例如下:
func Test(a int,b int) (sum int) { sum = a + b return sum } func main() { s := 0 //自动推导类型 创建函数类型变量 result := Test s = result(4,6) fmt.Println(s) fmt.Printf("%T ",result) }
1.7 函数作用域
(1)局部变量
前面我们定义的函数中,都经常使用变量。那么我们看一下如下程序的输出结果:
func Test() { a := 5 a += 1 } func main() { a := 9 Test() fmt.Println(a) }
最终的输出结果是9,为什么呢?在执行fmt.Println(a)语句之前,我们已经调用了函数Test(),并在该函数中我们已经重新给变量a赋值了。但是为什么结果没有发生变化呢?这就是变量的作用范围(作用域)的问题。在Test( )函数中定义的变量a,它的作用范围只在该函数中有效,当Test( )函数执行完成后,在该函数中定义的变量也就无效了。也就是说,当Test( )函数执行完以后,定义在改函数中所有的变量,所占有的内存空间都会被回收。
所以,我们把定义在函数内部的变量称为局部变量。
局部变量的作用,为了临时保存数据需要在函数中定义变量来进行存储,这就是它的作用。作用域只限定于本函数内部 从定义到函数结束。
并且,通过上面的案例我们发现:不同的函数,可以定义相同的名字的局部变量,但是各用个的不会产生影响。例如:我们在main( )函数中定义变量a,在Test( )函数中也定义了变量a,但是两者之间互不影响,就是因为它们属于不同的函数,作用范围不一样,在内存中是两个存储区域。
(2)全局变量
有局部变量,那么就有全局变量。所谓的全局变量:既能在一个函数中使用,也能在其他的函数中使用,这样的变量就是全局变量。也就是定义在函数外部的变量就是全局变量。全局变量在任何的地方都可以使用。案例如下:
//变量定义在函数外 var a int func Test() { a = 5 a += 1 } func main() { a = 9 Test() fmt.Println(a) }
注意:在上面的案例中,我们在函数外面定义了变量a,那么该变量就是全局变量,并且Test( )函数和main( )函数都可以使用该变量。该程序的执行流程是:先执行main( )函数,给变量a赋值为9,紧接着调用Test( )函数,在该函数中完成对变量a的修改。由于main( )函数与Test( )函数所使用的变量a是同一个,所以当Test( )函数执行完成后,变量的a已经变成了6. 回到main( )函数执行后面的代码,也就是 fmt.Println(a),输出的值就是6.
可能有同学已经发现该程序和我们前面写的程序还有一点不同的地方是:第一个程序我们是a:=9,但是第二个程序执行修改成了 a=9, 现在修改一下第二个程序如下:
var a int func Test() { a = 5 a += 1 } func main() { a := 9 Test() fmt.Println(a) }
该程序与上面的程序不同之处在于,该程序是a:=9,上面的程序是a=9.
现在大家思考一下该程序的结果是多少?
最终结果是9.
原因是:a:=9等价于
var a int
a=9
也就是定义一个整型变量a,并且赋值为9.
那么现在的问题是,我们定义了一个全局变量a,同时在main( )中又定义了一个变量也叫a,但是该变量是一个局部变量。
当全局变量与局部变量名称一致时,局部变量的优先级要高于全局变量。所以在main( )函数中执行fmt.Println(a)时输出的是局部变量a的值。但是Test( )函数中的变量a还是全局变量。
注意:大家以后在开发中,尽量不要让全局变量的名字与局部变量的名字一样。
所以大家,思考以下程序执行的结果:
var a int func Test() { a = 5 a += 1 fmt.Println("Test",a) } func main() { a := 9 Test() fmt.Println("main",a) }
结果是
Test 6 main 9
总结:
- 在函数外边定义的变量叫做全局变量。
- 全局变量能够在所有的函数中进行访问
- 如果全局变量的名字和局部变量的名字相同,那么使用的是局部变量的,小技巧强龙不压地头蛇
1.8 匿名变量
前面我们定义函数的时候,发现是不能在一个函数中,再次定义一个函数。如果我们想在一个函数中再定义一个函数,那么可以使用匿名函数,所谓匿名函数就是没有名字的函数。
如下所示:
func main() { var num int = 9 //匿名函数,注意没有函数名字 f := func() { num++ fmt.Println("匿名函数:",num) } f() fmt.Println("main函数:",num) }
在main( )函数中定义了一个匿名函数,定义的方式非常简单func(){ 函数体 },一定要注意的是在func的后面没有函数的名字,同时在这里定义的该匿名函数也没有参数。我们将定义好的匿名函数赋值给了变量f,那么变量f就是一个函数类型。要想执行该匿名函数,就可以通过f( )的方式去调用执行。
在这里,有一件非常有意思的事情,就是在匿名函数中可以直接访问main( )函数中定义的局部变量,并且在匿名函数中对变量的值进行了修改,最终会影响到整个main( )函数中定义的变量的值。所以上面两行输入都是10.
关于这一点,一定与上一节讲解的函数作用域进行区别。
匿名函数还有其它调用方式如下:
func main() { var num int = 9 //匿名函数,注意没有函数名字 f := func() { num++ fmt.Println("匿名函数:",num) } //定义一个没有参数,没有返回值的函数类型 type functype func() //声明变量 var f1 functype f1 = f f1() fmt.Println("main函数:",num) }
上面案例中,定义的匿名函数赋值给了变量f,那么f的类型就是函数类型,所以我们自己也可以定义一个函数类型的变量来调用匿名函数。但是上面的应用比较繁琐,实际用的比较少。
定义匿名函数时,直接调用
func main() { num := 9 func(){ num++ fmt.Println("匿名函数:",num) }() //该括号的作用就是直接调用匿名函数 fmt.Println("main函数:",num) }
该方式,需要在匿名函数的末尾加上小括号,表示调用。同时也不需要将定义好的匿名函数赋值给某个变量。
下面看一下怎样给匿名函数传递参数:
func main() { //匿名函数带参数 func(a,b int){ var sum int sum = a + b fmt.Println("和为:",sum) }(3,4) //调用时进行参数传递 }
或者如下方式:
func main() { //匿名函数带参数 f := func(a,b int) { var sum int sum = a + b fmt.Println("和为:", sum) } f(3,4) }
匿名函数如果有返回值,怎样进行处理呢?
func main() { 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 ",x,y) }
以上案例中定义了一个匿名函数,该匿名函数需要两个整型参数,同时指定了该函数返回值的名字是变量max与min
当执行到return时,让变量manx与min返回,赋值了变量x,y。x中存储的是max变量的值,y中存储的是min变量的值。
以上就是关于什么是匿名函数,以及匿名函数的使用。匿名函数最主要的功能就是实现了闭包。
1.9 递归函数
通过前面的学习知道一个函数可以调用其他函数。
如果一个函数在内部不调用其它的函数,而是自己本身的话,这个函数就是递归函数。
递归函数有两个特点:
- 在函数中调用自己
- 有明确结束递归的语句
下面来写一个计算一个数的阶乘的程序:
var res int = 1 func factorial(i int) { if i > 0{ res *= i factorial(i-1)//自己调用自己 } } func main() { var num int fmt.Print("请输入一个数:") fmt.Scanf("%d ",&num) factorial(num) fmt.Printf("%d的阶乘为%d ",num,res) }
二.工程管理
2.1 工作区介绍
通过前面函数的学习,我们能够体会到函数的优势,就是可以将不同的功能放在不同的函数中实现,主函数(main())可以直接调用。这样结构非常的清晰,也非常方面代码的管理。如果我们把所有的代码都写在main( )函数中,会出现什么样的情况呢?
代码混乱,非常不容易管理。但是现在我们面临了另外一个问题就是:我们所有自己定义的函数都写在了一个文件中,
如果我们做的项目代码量越来越多,那么该文件会变的非常臃肿,代码也会变得非常难管理。所以,我们在开发中,除了要定义函数,同时还要将代码放在不同的文件中。例如:我们定义了一个UserInfo.go文件,里面包含了用户的添加函数,修改函数,删除函数等操作。
这就涉及到项目的工程管理也就是怎样对项目中的文件进行管理。
为了更好的管理项目中的文件,要求将文件都要放在相应的文件夹中。GO语言规定如下的文件夹如下:
(1)src目录:用于以代码包的形式组织并保存Go源码文件。(比如:.go .c .h .s等)
(2)pkg目录:用于存放经由go install命令构建安装后的代码包(包含Go库源码文件)的“.a”归档文件。
(3)bin目录:与pkg目录类似,在通过go install命令完成安装后,保存由Go命令源码文件生成的可执行文件。
以上目录称为工作区,工作区其实就是一个对应于特定工程的目录。
目录src用于包含所有的源代码,是Go命令行工具一个强制的规则,而pkg和bin则无需手动创建,如果必要Go命令行工具在构建过程中会自动创建这些目录
2.2 创建同级目录
(1) 创建src目录,在改目录下创建go源码文件
在项目文件夹下新建src目录,如下图所示:
在src目录下创建不同的go源码文件,如下图所示:
在src目录下创建main.go文件和test.go文件(注意:这个两个文件是在同一个目录下面,都是在src目录下面)
main.go 文件下的代码如下所示:
package main import "fmt" func main() { Test() fmt.Println("main") }
test.go 文件下的代码如下所示:
package main //必须与main.go同一个包 import "fmt" func Test() { fmt.Println("Test") }
现在已经完成两个文件代码的编写,接下来的问题是,我们怎样在main.go文件中的入口函数main( )中调用test.go文件中的Test( )函数呢?这就需要设置环境变量GOPATH属性。如果要完成不同文件中函数的调用,必须设置GOPATH,否则,即使文件处于同一工作目录(工作区)下,也是无法完成调用的。
(2)GOPATH设置
GOPATH设置的具体步骤如下:
配置完成后,可以测试一下是否配置成功
(3)在GOLand编辑器上设置
最后点击Apply然后OK。
(4)在main.go文件中完成对test.go文件中函数的调用
func main() { Test() fmt.Println("main") }
最后编译执行。
注意:同一个目录下不能定义不同的package。
3.3 创建不同级目录
在上一小节中,将不同的go源代码文件都放在了同一个目录下面,如果将go源代码文件放在不同的目录下面应该怎样进行处理呢?
具体的步骤如下:
(1)在src目录下新建一个目录userinfo
(2)在userinfo目录下创建UserLogin.go和CreateUser.go文件
UserLogin.go内容如下:
package userinfo import "fmt" //这里函数名的首字母是大写的 func UserLogin() { fmt.Println("用户登录成功") }
CreateUser.go内容如下:
package userinfo import "fmt" ////这里函数名的首字母是小写的 func createuser() { fmt.Println("用户创建成功") }
(3)然后再main.go中调用
我们发现在main.go中只能调用userinfo包下的UserLogin函数,无法调用createuser函数,这是为什么呢?
原因是,在Go语言中,函数名的首字母大写,表示此函数是一个公共函数,可以被包外访问;函数名⾸字母⼩写,表示此函数是一个私有函数,仅包内成员可以访问。
在UserLogin.go中就可以被调用。
(4)继续在main.go中调用userinfo包下的函数
package main import ( "fmt" "userinfo" ) func main() { fmt.Println("main") userinfo.UserLogin() }
执行结果如下:
main 用户登录成功 用户创建成功
总结一下:
- 不同目录,包名不一致(自定义包)。
- main.go中调用其他包中的方法时,一定要导包,并且调用的方式是:包名.函数名 的方式。
- 当定义一个函数时,如果允许包外的函数调用它,则其首字母必须大写。
- 当定义一个函数时,如果不允许包外的函数调用它,只允许包内调用,则其首字母必须小写。
最后说一下导包的问题
在上面的案例中,要使用包,必须要进行导入,可以通过关键字进行import进行导入,它会告诉编译器你想引用该包内的代码。
如果导入的是标准库中的包(GO语言自带,例如:”fmt”包)会在安装 Go 的位置找到。 Go 开发者创建的包会在 GOPATH 环境变量指定的目录里查找。所以,import关键字的作用就是查找包所在的位置。
如果编译器查遍 GOPATH 也没有找到要导入的包,那么在试图对程序执行 run 或者 build的时候就会出错。
注意:如果导入包之后,未调用其中的函数或者类型将会报出编译错误。
我们常规的导包方式是用import关键字一个个导入。
例如:
表示导入三个包,有GO语言自带的包,也有我们自定义的包。但是,这种写法可能比较麻烦,所以为了简化也可以采用如下的方式进行导包:
这种方式,使用的频率是非常高的。