Go语言之函数
函数是组织好的、可重复使用的、用来实现单一或相关联功能的代码段,其可以提高应用的模块性和代码的重复利用率
Go 语言支持普通函数、匿名函数和闭包,从设计上对函数进行了优化和改进,让函数使用起来更加方便
Go 语言的函数本身可以作为值进行传递,支持匿名函数和闭包( closure ),函数可以满足接口
声明函数
普通函数需要先声明才能调用。一个函数的声明包括参数和函数名等,编译器通过声明才能了解函数应该怎样在调用代码和函数体之间传递参数和返回参数。
1、普通函数的声明形式:
Go 语言的函数声明以func 标识,后面紧接着函数名、参数列表、返回参数列表及函数体,具体形式如下:
func 函数名(参数列表)(返回参数列表){
函数体
}
说明:
- 函数名:由字母、数字、下画线组成。其中,函数名的第一个字母不能为数字。在同一个包内,函数名称不能重名,调用可能会调用错
- 参数列表: 一个参数由参数变量和参数类型组成,例如:func foo ( a int , b string ),其中,参数列表中的变量作为函数的局部变量而存在。
- 返回参数列表:可以是返回值类型列表,也可以是类似参数列表中变量名和类型名的组合。函数在声明有返回值时,必须在函数体中使用return语句提供返回值列表
- 函数体:能够被重复调用的代码片段
2、参数类型的简写:
在参数列表中,如有多个参数变量,则以逗号分隔;如果相邻变量是同类型,则可以将类型省略。例如
func add (a, b int )int {
return a + b
}
以上代码中, a 和b 的参数均为int 类型,因此可以省略a 的类型,在b 后面有类型说明,这个类型也是a 的类型。
3、函数的返回值:
Go 语言支持多返回值,多返回值能方便地获得函数执行后的多个返回参数, Go 语言经常使用多返回值中的最后一个返回参数返回函数执行中可能发生的错误,如下:
conn , err := connectToNetwork()
在这段代码中, connectToNetwork 返回两个参数, conn 表示连接对象, err返回错误
Go 语言既支持安全指针,也支持多返回值,因此在使用函数进行逻辑编写时更为方便。
3.1 同一种类型返回值:
如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型,使用return语句返回时,值列表的顺序需要与函数声明的返回值类型一致,代码如下:

注意:上面test()函数,定义了两个都是int类型返回值,那么return也要相应的有对应的返回值,并和定义顺序一一对应
3.2,不同类型返回值,如图:

代码执行结果如下:

注:函数虽然有返回值,但是调用函数的时候,可以不定义接收值,如图:

结果可以正常打印而不报错,如图:

4、定义不定参数函数:
不定参数函数就是参数不确定的函数,表示可接受任意数量的int参数,在Go语言中,能够接受可变数量的参数,但他们的类型必须和函数签名指定的类型相同,要指定不定参数,可使用三个点(…),不定参数只能出现在函数的最后一个参数,因此一个函数中只能有一个不定参数
下面例子中表示定义函数,参数为不定参数的int类型,如图:

运行结果如下:

从上图看出,不定参数的类型实际是一个int类型的切片
下面定义不定参数的函数,参数类型可以为任意类型,使用interface,如图:


5、使用具名返回值(带有变量名的返回值):
具名返回值让函数能够在返回前将值赋值给具名变量,有助于提升函数的可读性,要使用具名返回值,可在函数签名的返回值部分指定变量名,如图:

上图中的return后没有加返回值,因此称为裸return语句,在下方调用函数时,将按照声明顺序返回具名变量,跟赋值的先后顺序无关
6、使用递归函数:
递归函数是不断调用自己直到满足特定条件的函数,要在函数中实现递归,可将调用自己的代码作为终止语句中的返回值,如图:

上图中的test函数最后一行,没有返回值,而是调用自己,再次执行函数,直到a的值大于等于5,才终止函数,代码执行结果如下:

7、将函数作为值进行传递
在go语言中,可将函数视为一种类型,因此可将函数赋值给变量,在通过变量来调用他们,如图:

上图中,将字符串类型函数赋值给fn,将fn传递给函数test(),函数test()的参数也为字符串类型的函数,返回类型也为字符串
代码执行结果如下:

例子2:

代码执行结果如下:

8、调用函数:
函数在定义后,可以通过调用的方式,让当前代码跳转到被调用的函数中进行执行。调用前的函数局部变量都会被保存起来不会丢失;被调用的函数结束后,恢复到被调用函数的下一行继续执行代码,之前的局部变量也能继续访问。
函数内的局部变量只能在函数体中使用,函数调用结束后,这些局部变量都会被释放并且失效。
Go 语言的函数调用格式如下:
返回值变量列表=函数名(参数列表)
- 函数名: 需要调用的函数名。
- 参数列表: 参数变量以逗号分隔,尾部无须以分号结尾。
- 返回值变量列表: 多个返回值使用逗号分隔。
例如:result := add(l , l)
匿名函数
Go 语言支持匿名函数,即在需要使用函数时,再定义函数,匿名函数没有函数名,只有函数体,函数可以被作为一种类型被赋值给函数类型的变量,医名函数也往往以变量方式被传递。匿名函数经常被用于实现回调函数、闭包等。
匿名函数的定义格式如下:
func (参数列表)(返回参数列表){
函数体
}
匿名函数的定义就是没有名字的普通函数定义
1、匿名函数可以在声明后调用,此时函数只能执行一次,如图:

注意:匿名函数func()要写在主函数main()中,否则会报错
2、将匿名函数赋值给变量,此时的匿名函数就可以多次调用了,如图:

匿名函数的用途非常广泛,匿名函数本身是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。
3、医名函数用作回调函数
下面的代码实现对切片的遍历操作,遍历中访问每个元素的操作使用匿名函数来实现。用户传入不同的匿名函数体可以实现对元素不同的遍历操作,如图:

代码执行结果如下:

当函数的参数为匿名函数时,此时的匿名函数不需要写{},比如上面的f func(int) 即可
4、使用匿名函数实现操作封装
go语言提供的flag包可以解析命令行的参数
下面这段代码将匿名函数作为map的值,通过命令行参数动态调用匿名函数,如下:

说明:
- 第8行,声明变量skillParam,通过flag包解析参数,第一个参数为skill,第二个参数为默认值,第三个为说明
- 第11行,flag的包的固定语法,解析命令行参数,解析完成后,skillParam指针变量将指向命令行传入的值
- 第12行,定义一个map,键类型为字符串,值类型为匿名函数func()
- 第13-18行,通过声明式填充方式初始化键值对,值也是匿名函数类型
- 第20-23行,判断是否存在这个键,如果有那么就调用,否则打印没有发现,注意,f 表示值,如果存在这个键,那么就f就是这个键对应的值,也就是对应的匿名函数,此时通过f()即可调用此函数
在命令行执行命令,将参数传递给skill,打印结果如图:

闭包–引用了外部变量的匿名函数
闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使己经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量。因此,简单的说:函数+引用环境=闭包
同一个函数与不同引用环境组合,可以形成不同的实例,如图:

一个函数类型就像结构体一样,可以被实例化。函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。
1、在闭包内部修改引用的变量:
闭包对它作用域上部变量的引用可以进行修改,修改引用的变量就会对变量进行实际修改,如下:

运行结果如下:

从上图可以看出,变量arr1的值已经被修改,在匿名函数外部访问依旧是修改之后的值
注:闭包对捕获的外部变量不是通过传值方式访问,而是通过引用的方式访问
2、闭包的记忆效应:
被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量, 变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。
下面例子,通过函数与闭包,实现了一个累加器的功能,如图:

说明:
- 第5行:定义一个累加器生成函数,返回值为带变量的int类型,变量名为匿名函数
- 第6行:返回一个闭包函数
- 第7行:对引用Accumulate参数变量进行累加,累计后的值通过闭包的返回值返回
- 第12行:创建一个累加器,初始值为1,返回的accumulate是类型为func()int的函数变量
- 第13行:调用accumulate(),从第6行到第9行开始执行匿名函数逻辑
- 第15行:打印函数的地址
执行结果如下:

从上图结果可以看出:第一次的打印结果为2,因为初始值为1,执行第一个匿名函数修改了value的值,返回为2,当第二次调用的时候,闭包函数依旧记得value的值为2,再次累加,因此返回的值为3,每调用一次accumulator 都会自动对引用的变量进行累加,下面的调用原理相同,最后看两次打印的函数地址不同,因为他们是两个不同的闭包实例
3、闭包经常出现的共享循环变量问题
闭包中获取循环变量的时候,打印的结果可能不及预期,如图:

预期结果应该为a-g,实际打印结果为:

从上图看出,结果不及预期,因为main所在的goroutinue和go func()的goroutinue两个是无序进行的,当执行闭包函数的时候,v的值可能已经是最后一个,也可能是随机,因此闭包函数可能每次获取的都是同一个v的值,要想解决此问题,只需要将循环变量的值赋值给一个新的值,每次循环都会创建一个新的值,如图:

打印结果如下:

延迟执行语句(defer)
Go 语言的defer 语句会将其后面跟随的语句进行延迟处理。在defer 归属的函数即将返回时,将延迟处理的语句按defer 的逆序进行执行,也就是说,先被defer 的语句最后被执行,最后被defer 的语旬,最先被执行。
1、多个延迟执行语旬的处理顺序
下面是将一系列的数值进行打印,如图:

执行结果如下:

从上图可以看出,代码的延迟顺序与最终的执行顺序是反向的,延迟调用是在defer 所在函数结束时进行,函数结束可以是正常返回时,也可以是发生宿机时。
2、使用延迟执行语句在函数退出时释放资源
处理业务或逻辑中涉及成对的操作是一件比较烦琐的事情,比如打开和关闭文件、接收请求和回复请求、加锁和解锁等。在这些操作中, 最容易忽略的就是在每个函数退出处正确地释放和关闭资源。defer 语句正好是在函数退出时执行的语旬,所以使用defer 能非常方便地处理资源释
(1) 使用延迟井发解锁
下面例子在函数中并发使用map ,为防止竞态问题,使用sync.Mutex 进行加锁,如下:

说明:
- 第9行:实例化一个map,键是string类型,值是int
- 第10行:map 默认不是并发安全的,准备一个sync.Mutex 互斥量保护map的访问
- 第13行:readValue () 函数给定一个键,从map 中获得值后返回,该函数会在并发环境中使用,需要保证并发安全
- 第14行:使用互斥量加锁
- 第15行:使用defer 语句添加互斥量解锁,该语句不会马上执行,而是等readValue()返回时才会被执行
- 第16行:从map中查询值并返回
(2)、延迟语句配置return返回的例子,如图:


再看一个定义命名返回值并修改数据的例子,如图:


上图中的结果返回11而不是10 ,是因为Go 语言的 ”命名返回值 + defer 修改” 机制在起作用,命名返回值的意思就是在函数的返回值位置也指定了名字(上图中的result int),return result实际是将result放入返回栈中,如果是命名返回值,那么此时栈内为result的引用,defer仍然可以修改此引用的值,修改后最后一起返回
如果是匿名返回值,那么结果就不一样了,如图:


从上图看出,defer修改的值为11,但是return最终返回的值为10,因为匿名返回值在return的时候传递的是值的副本,因此defer修改不会影响到原来的值
注意:defer是在return语句执行完成后并且在函数真正返回给调用方前执行,因此先打印defer,然后打印return返回内容
关于互斥锁的内容可参考链接:https://blog.ywdevops.cn/index.php/2022/01/29/mutex/
2、使用延迟释放文件旬柄
文件的操作需要经过打开文件、获取和操作文件资源、关闭资源几个过程,如果在操作完毕后不关闭文件资源,进程将一直无法释放文件资源,
下面例子将根据文件名获取文件大小,函数中需要打开文件、获取大小与关闭文件,如图:

说明:
- 第8行:定义获取文件大小的函数,返回值是64 位的文件大小值
- 第9行:使用OS 包提供的函数Open(),根据给定的文件名打开一个文件,并返回操作文件用的句柄和操作错误
- 第10行:如果打开的过程中发生错误,如文件没找到、文件被占用等,将返回文件大小为0
- 第14行:获取文件的状态信息
- 第15行:如果文件状态信息有错误,将返回0
- 第18行:根据文件状态信息来获取文件的大小信息
- 第19行:返回文件的大小
- 第13行:在文件正常打开后,使用defer语句,将f.close()延迟调用,注意:不能将这一样代码放在第9行之后,因为一旦文件打开失败,f 将为空,比如文件不存在,此时在执行defer来关闭文件时,将触发宕机错误
阻塞函数:当这个函数不执行完,函数所在线程就一直停止在这里不动。意思就是这个函数会一直等待,只有在得到结果之后才会返回。同时它也不允许程序调用另一个函数。
打印结果表示文件的大小,如下:

3、当遇到panic的时候,defer还是会执行


注意:defer只能用在函数或者方法前面,不能用在其他前面,例如for 、if 等
4、defer语句虽然在函数结束的时候执行,但是如果延迟执行函数中有参数,那么先计算参数值,如图:


上图中的三个延迟函数,一个first(),还有两个Println(),都有参数,但是第一个参数num就是一个变量,单独的变量不能执行任何计算,后面的两个fmt.Println()中的参数second和third可进行计算,因此先执行second然后执行third,在执行完End后,开始倒着来打印defer的函数,third这里打印的是third函数的return,second打印的是second函数的return,first因为只传递了num参数,因此,只打印first中的fmt,不打印return,如果将defer first(num)改为defer fmt.Println(first(num))就会打印return的值了,如图:
5、在函数中使用defer语句延迟调用匿名函数时,如果该函数有返回值,则可能有两种情况,一种是返回值未设置变量名,另一种是为返回值设置变量名
(1)、test函数返回值未设置变量名,返回的是函数内部定义的i的值,此时defer是不能修改函数的返回值,因此返回值还是i的零值,也就是0,如图:


(2)、test函数返回值设置了变量名,defer可以修改test函数的返回值,最终的返回值为2,如图:




