第四篇:函数
4.1函数基础
4.1.1 认识函数
在认识函数之前,我们先来看一个场景:
1、在程序里,有4个地方要判断当前用户是不是vip
isVip := true
if isVip == true {
fmt.Println("尊敬的vip,你好")
}else{
fmt.Println("给我滚")
}
.....
if isVip == true {
fmt.Println("尊敬的vip,你好")
}else{
fmt.Println("给我滚")
}
....
if isVip == true {
fmt.Println("尊敬的vip,你好")
}else{
fmt.Println("给我滚")
}
....
if isVip == true {
fmt.Println("尊敬的vip,你好")
}else{
fmt.Println("给我滚")
}
这么写有什么问题啊?是不是有很多重复代码,代码很冗长。
现在第二个问题来了:
2、产品经理说,“给我滚”太嚣张了,要用“请滚”。
isVip := true
if isVip == true {
fmt.Println("尊敬的vip,你好")
}else{
fmt.Println("请滚")
}
.....
if isVip == true {
fmt.Println("尊敬的vip,你好")
}else{
fmt.Println("请滚")
}
....
if isVip == true {
fmt.Println("尊敬的vip,你好")
}else{
fmt.Println("请滚")
}
....
if isVip == true {
fmt.Println("尊敬的vip,你好")
}else{
fmt.Println("请滚")
}
发现什么问题了没有,你4个地方写了这个功能,要修改的话,是不是得一个个找出来,一个一个修改啊?是不是很难维护你的代码?
下面就要引出我们的函数了。
什么是函数
在程序中,函数就是具备某一功能的工具。
怎么理解这句话呢,我举个例子。
老王现在要去修下水道,他带上了螺丝刀和扳手,遇到应用场景,直接拿出来用就行了。而不是每次都需要重新造一个工具出来。
在这个例子里,螺丝刀和扳手就相当于函数。遇到应用场景我直接用就行了,不需要重新再写功能。
事先将工具准备好,就叫做函数的定义
遇到应用场景拿出来用,就叫做函数的调用
为什么要有函数
不用函数会造成:1、代码冗长 ;2、可维护性差;3、可读性差
###4.1.2函数定义
格式如下:
func 函数名(参数1 类型,参数2 类型,...)(返回值名1 类型,返回值名2 类型,...){
函数体代码...
return 返回值1,返回值2
}
返回值和参数也都可以没有,就可以写成下面这样:
func 函数名 (){
函数体代码...
}
###4.1.3函数的调用
函数名(),我们用函数名加()的形式去调用函数。
如果需要传参数,那就要在()里写上参数,如:函数名(参数1,参数2)
如果需要接受返回值,那就声明一个或者多个变量去接收,如:变量名:=函数名(参数1,参数2…)
###4.1.4函数的返回值
1、函数中返回值形式如下:
return 返回值
return 返回值1, 返回值2
2、在需要有返回值的时候,func后面一定要写上返回值的类型,如:
func 函数名()int{}
func 函数名()(int, string){}
3、命名返回值
我也可以给返回值指定变量名,如:
func 函数名()(aa int,bb int){
aa = 3
bb = 4
return //我后面什么都不写,函数就会在代码体中找到aa和bb并返回
}
4、返回值使用场景
什么时候该用返回值:
调用函数,经过一系列的操作,最后要拿到一个明确的结果,则必须要有返回值。
什么时候不用返回值:
调用函数,仅仅只是执行一系列的操作,最后不需要得到什么结果,则无需有返回值
###4.1.5函数的参数
1、分类
实参:函数定义时写在()内的变量名,叫做实参
形参:函数调用时写在()内的值,叫做形参
如:
func bigNum(x,y int)int{ //这里的x和y就是形参
if x>=y {
retrun x
}else{
retrun y
}
}
var a,b int= 1,2
var biggest = bigNum(a,b) //这里的a和b就是实参
2、形参中的变长形参
有这么一种情况,我不知道我要传递的参数是几个,可能是2个,可能是3个,也可能更多。这个时候怎么办呢。这就需要变长形参了。我们在参数类型前加上“…”来接收多数据,最后得到一个该类型的切片。如:
func myFu (a ...int){
fmt.Println(a)
}
myFu(1,2,3,4,5,6) //结果 [1 2 3 4 5 6]
##4.2函数进阶
###4.2.1函数变量及匿名函数
有这么一句话,“函数即变量”。
什么意思啊?我们都知道变量名绑定的是一个内存地址,那函数名存的啥啊?代码看看:
func test (){
fmt.Println("我特么是个函数体代码")
}
fmt.Println(test) //结果:0x48df40
看到没?函数名指向的也是一个内存地址。
那么我们就想了,如果我把这个内存地址给一个变量,会是什么结果呢?
func test (){
fmt.Println("我特么是个函数体代码")
}
test2 := test
test2() //控制台上我们得到了“我特么是个函数体代码”
这个例子就说明了,函数即变量,我们定义函数其实就是定义了一个变量,只是这个变量的类型是函数类型。
我们来看看函数类型长什么样:
func test1(){
fmt.Println("我是test1")
}
func test2(a,b int){
fmt.Println("我是test2")
}
func test3(a,b int)string{
fmt.Println("我是test3")
return "啦啦啦"
}
fmt.Println(reflect.TypeOf(test1)) //func()
fmt.Println(reflect.TypeOf(test2)) //func(int, int)
fmt.Println(reflect.TypeOf(test3)) //func(int, int) string
现在我们知道了函数类型长什么样,是不是可以像定义变量一样去定义一个函数啊?
我们怎么定义数组的?是不是 变量名 := 类型{值}
。我们来试试看用这个方法来定义一个函数,代码如下:
test1 := func(){fmt.Println("我是test1")}
test2 := func(x int){fmt.Printf("我是test2,我有参数%v\n",x)}
test3 := func()string{return "我有返回值"}
//我们来看看test1的值是什么?是不是和func定义的函数名一样也是内存地址
fmt.Println(test1) // 结果:0x48dec0
//那结果是个函数的内存地址,我加括号是不是可以直接运行呢?
test1() // 我是test1
test2(10) // 我是test2,我有参数10
fmt.Println(test3()) // 我有返回值
我们接着往下思考。
上面这个例子中,test1、test2、test3的值是内存地址,那么,变量名等号后面的,是不是也是内存地址呢?试试憋:
fmt.Println(func(){fmt.Println("我是test1")})
//结果:0x48ff10
那既然是函数内存地址,加括号()就能运行咯?再试试:
func(){fmt.Println("我是test1")}()
//结果:我是test1
这个就引出了我们的一个概念,匿名函数。形如func(){fmt.Println("我是test1")}
这样没有名字的函数就叫做匿名函数
###4.2.2 函数的嵌套
1、函数的嵌套调用
func max(a,b int)int{
if a >= b {
return a
}else{
return b
}
}
func max4(a,b,c,d int)int{
res1 := max(a,b)
res2 := max(res1,c)
res3 := max(res2,d)
return res3
}
fmt.Println(max4(1,2,3,4))
2、函数的嵌套定义
在go语言里,不支持一个func下再使用func定义函数。
那怎么才能实现嵌套定义呢?还记不记得我们说过的匿名函数,我们可以用变量声明+匿名函数的方式去实现嵌套定义。如下:
func test1(){
fmt.Println("我是最外层函数")
//声明变量+匿名函数
test2 := func(){
fmt.Println("我是里层函数")
}
test2()
}
test1()
//结果:我是最外层函数
// 我是里层函数
###4.2.3作用域
1、语法块
语法块就是一个代码块,一个{}内的代码就是一个语法块,语法块内的变量叫做局部变量,如:
if 2>1 {
a := 3
}
for i := 1;i < 3;i++ {
fmt.Println(i)
}
func test(){
fmt.Println("我在test函数的语法块里")
}
我们也可以自己定义一个语法块,用{}把代码括起来就行,如下:
func test(){
fmt.Println("我在test函数的语法块里")
{
fmt.Println("我在自定义语法块里")
}
}
2、作用域
作用域就是变量的有效范围。
分类:全局作用域,局部作用域
局部作用域:一个语法块,就是一个局部作用域,定义的变量叫做局部变量,仅在当前语法块有效
全局作用域:在{}以外的地方定义的变量,叫做全局变量,全局有效
变量的查找顺序:局部—>…—>全局
package main
import "fmt"
var a = 10
func test(){
a := 100
{
a = 200
}
fmt.Println(a)
}
func main() {
fmt.Println(a) //结果:10
test() //结果:200
}
程序运行逻辑:
1、首先执行main函数里的fmt.Println(a)
2、局部作用域里找变量a,没有
3、全局作用域里找变量a,找到了,10
4、执行main函数里的)test()
5、局部作用域里找test,没有
6、全局作用域里找test,找到了,然后执行test里的代码
7、a:=100,那就在test函数的局部作用域声明了一个局部变量
8、{a = 200},这个局部作用域里,没有变量a,就往外层找,找到上一层中定义的a,然后改成200
9、fmt.Println(a),在局部作用域里找变量a,找到了,这时的值是200
###4.2.4闭包函数
什么是闭包函数:
闭包就是闭合和包起来的意思。
什么是包起来?就是一个函数被另一个函数包起来,这有点像我们之前讲的嵌套定义,不过这里外层函数的返回值是里层函数的函数名,如下:
func test()func(){
inter := func() {
fmt.Println("我被包起来了")
}
return inter
}
什么是闭合?就是内层函数引用的外部变量有且只能来自于外层函数,如下:
func test()func(){
var a = 0
inter := func() {
a ++ //我引用了外部变量,这个变量来自于它的外层函数
fmt.Println(a)
}
return inter
}
f := test() //f就相当于inter
f() //a ++ ,修改它外层作用域里的a,a的值修改后为1
f() //a ++ ,依旧是修改外层函数的a,a的值当前为1,修改后为2
闭包函数的应用:
1、延时计算
2、装饰器,但在go里没有现成的封装好的装饰器语法,得自己写
4.2.5panic恐慌
panic在英语里是恐慌的意思,程序员的恐慌无疑就是程序运行时报错。
程序报错可以分为两种:1、编译时出错;2、运行时出错
1、编译时出错如下:
2、panic恐慌如下:
4.2.5.1 panic恐慌到程序崩溃
下面是panic到程序崩溃的流程图:
说明:
1、程序一步步运行到test2下的a := myArry[4]
引发panic恐慌
2、剩下的代码不会执行,控制权转移给test2函数
3、接着test2函数退出,控制权转移给test1函数。
4、接着test1函数退出,控制权转移给main函数
5、main函数退出,程序崩溃
4.2.5.1panic函数
我们可以通过panic函数,手动触发panic恐慌。
panic(“提示信息”),如下:
func main(){
fmt.Println("进入主函数")
panic("蠢货,我是panic,怕不怕")
fmt.Println("退出主函数")
}
//进入主函数
//panic: 蠢货,我是panic,怕不怕
//goroutine 1 [running]:
//main.main()
//D:/GoWorks/src/demo1/test.go:7 +0x80
//Process finished with exit code 2
4.2.6recover恐慌平息
panic恐慌发生了,怎么办?当然得处理,不然会被炒鱿鱼!
如何处理,这就是我们要说的recover恐慌平息
用法很简单,如下:
errMessage := recover()
//recover函数有一个返回值,用来接收panic恐慌的报错信息
//recover函数在panic恐慌发生后运行,来平息恐慌
注意:
你们有没有发现什么问题?我panic恐慌发生后,后面的代码就不会再执行了,而我recover又要在panic恐慌之后运行。是不是很矛盾?
你们想的没错,的确是这样,recover()放在panic之后是不会执行的:
func main(){
fmt.Println("进入主函数")
panic("蠢货,我是panic,怕不怕")
errMessage := recover()
fmt.Println(errMessage)
}
//进入主函数
//panic: 蠢货,我是panic,怕不怕
//goroutine 1 [running]:
//main.main()
//D:/GoWorks/src/demo1/test.go:9 +0x80
//Process finished with exit code 2
那既然这样,我recover还有什么意义啊?
别急,这就要引出我们下一个知识点,defer延时函数
###4.2.7defer延时函数
1、什么是defer延时函数
延时很好理解,就是我预约之后过段时间再执行嘛。
func main(){
defer fmt.Println("我是第一个defer")
fmt.Println("哈哈哈哈")
}
//哈哈哈哈
//我是第一个defer
函数先执行了defer之后的代码,最后回过来再执行defer函数
func main(){
defer fmt.Println("我是第一个defer")
defer fmt.Println("我是第二个defer")
defer fmt.Println("我是第三个defer")
fmt.Println("哈哈哈哈")
}
//哈哈哈哈
//我是第三个defer
//我是第二个defer
//我是第一个defer
1、我第一个defer告诉函数,我要最后一个执行。
2、第二个defer也告诉函数,我要最后一个执行。程序就说了,这不行啊,已经有人预约了最后一个执行了,我把你放在它后面把,你倒数第二个执行。
3、第三个defer也出来了。程序说,不行,前面有两个人预约了,你不能最后一个,这样你倒数第三个。
2、defer实现原理:
那多个defer,程序是怎么实现的呢?堆积木大家都堆过,多个defer就像堆积木:
先进后出!
3、怎么用defer函数:
defer函数多用于处理panic恐慌:
func main(){
defer func() {
message := recover()
fmt.Println(message)
}()
panic("我是你的噩梦")
fmt.Println("哈哈哈哈")
}
//我是你的噩梦
1、程序运行到panic的时候,后面就不执行了,控制权给了main()函数
2、正常情况下,程序会直接崩溃,但main函数说,等等,我还没执行完,有个defer预约了
3、执行defer函数,运行到recover之后,panic恐慌就被平息了
4、函数正常退出