Go语言基础之函数
Go语言基础之函数
函数是组织好的、可重复使用的、用于执行指定任务的代码块。本文介绍了 Go 语言中函数的相关内容。
Go 语言中支持函数、匿名函数和闭包,并且函数在 Go 语言中属于 “一等公民”。
1. 介绍
Go语言里面拥三种类型的函数:
- 普通的带有名字的函数
- 匿名函数或者 闭包(lambda) 函数
- 方法:接收器中的函数
与其他语言的差异
可以有多个返回值
所有参数都是值传递:slice,map,channel会有传应用的错觉
函数可以作为变量的值
函数可以作为参数和返回值
原则
- 函数与变量名首字母小写,本包可见(private),首字母大写,整个项目可见(public)
- 同包内函数与变量名不能重复
2. 函数的定义
Go语言是编译型语言,所以函数定义的顺序是无关紧要的。
Go 语言中定义函数使用func
关键字,具体格式如下:
func 函数名(形式参数列表)(返回值列表){ |
其中:
函数名:由字母、数字、下划线组成。但函数名的第一个字母不能是数字。在同一个包内,函数名也称不能重名(包的概念详见后文)。
参数:参数由参数变量和参数变量的类型组成,多个参数之间使用
,
分隔。返回值:Go函数支持多返回值。 函数可以有返回值也可以没有。如果一个函数在声明时,包含返回值列表,该函数必须以 return语句结尾。返回值由返回值变量和其变量类型组成,也可以只写返回值的类型,多个返回值必须用
()
包裹,并用,
分隔。函数体:实现指定功能的代码块。
下面的例子展示了一个简单的加减乘除运算的函数:
package main |
常见使用语法:
// 无参有匿名返回值 |
3. 函数的调用
定义了函数之后,我们可以通过函数名()
的方式调用函数。 例如我们调用上面定义的两个函数,代码如下:
func main() { |
注意,调用有返回值的函数时,可以不接收其返回值。
函数调用时,Go语言没有默认参数值。
不需要的返回值可使用匿名变量
_
来占位:- 任何类型都可以赋值给它,但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。
- 匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。
- 在 Lua 等编程语言里,匿名变量也被叫做哑元变量。
4. 参数
Go语言中函数的传递的参数都是拷贝(都是副本,不会影响原数据)
类型简写
函数的参数中如果相邻变量的类型相同,则可以省略类型,例如:
func intSum(x, y int) int { |
上面的代码中,intSum
函数有两个参数,这两个参数的类型均为int
,因此可以省略x
的类型,因为y
后面有类型说明,x
参数也是该类型。
可变参数
可变参数是指函数的参数数量不固定。Go语言中的可变参数通过在参数名后加...
来标识。
注意:可变参数通常要作为函数的最后一个参数。
固定类型
举个例子:
func intSum2(x ...int) int { |
调用上面的函数:
ret1 := intSum2() |
固定参数搭配可变参数使用时,可变参数要放在固定参数的后面,示例代码如下:
func intSum3(x int, y ...int) int { |
调用上述函数:
ret5 := intSum3(100) |
任意类型
如果你希望传任意类型,可以指定类型为interface{}
func MyPrintf(args ...interface{}) { |
传递可变参数
可变参数变量是一个包含所有参数的切片,如果要将这个含有可变参数的变量传递给下一个可变参数函数,可以在传递时给可变参数变量后面添加...
,这样就可以将切片中的元素进行传递,而不是传递可变参数变量本身。
// 实际打印的函数 |
参数传递
实参通过值传递的方式进行传递,因此函数的形参是实参的拷贝,对形参进行修改不会影响实参,但是,如果实参包括引用类型,如指针、slice(切片)、map、function、channel 等类型,实参可能会由于函数的间接引用被修改。
package main |
5. 返回值
Go语言中通过return
关键字向外输出返回值。
Go语言的函数支持多返回值。我们在开发时候会习惯返回运算结果与一个error对象。
Go语言中函数支持多返回值,函数如果有多个返回值时必须用()
将所有返回值包裹起来。
同一类型返回值
如果返回值是同一种类型,则用括号将多个返回值类型括起来,用逗号分隔每个返回值的类型。
使用 return 语句返回时,值列表的顺序需要与函数声明的返回值类型一致。
纯类型的返回值对于代码可读性不是很友好,特别是在同类型的返回值出现时,无法区分每个返回参数的意义。
func typedTwoValues() (int, int) { |
带有变量名的返回值
函数定义时可以给返回值命名,并在函数体中直接使用这些变量,最后通过return
关键字返回。
- Go语言支持对返回值进行命名,这样返回值就和参数一样拥有参数变量名和类型。
- 命名的返回值变量的默认值为类型的默认值,即数值为 0,字符串为空字符串,布尔为 false、指针为 nil 等。
func calc(x, y int) (sum, sub int) { |
返回值补充
当我们的一个函数返回值类型为slice时,nil可以看做是一个有效的slice,没必要显示返回一个长度为0的切片。
func someFunc(x string) []int { |
6. 变量作用域
全局变量
全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。
package main |
局部变量
函数内定义的变量
局部变量又分为两种: 函数内定义的变量无法在该函数外使用,例如下面的示例代码main函数中无法使用testLocalVar函数中定义的变量x:
func testLocalVar() { |
如果局部变量和全局变量重名,优先访问局部变量。
package main |
语句块定义的变量
接下来我们来看一下语句块定义的变量,通常我们会在if条件判断、for循环、switch语句上使用这种定义变量的方式。
func testLocalVar2(x, y int) { |
还有我们之前讲过的for循环语句中定义的变量,也是只在for语句块中生效:
func testLocalVar3() { |
7. 函数类型与变量
在Go语言中,函数也是一种类型,可以和其他类型一样保存在变量中。
func fire() { |
定义函数类型
我们可以使用type
关键字来定义一个函数类型,具体格式如下:
type calculation func(int, int) int |
上面语句定义了一个calculation
类型,它是一种函数类型,这种函数接收两个int类型的参数并且返回一个int类型的返回值。
简单来说,凡是满足这个条件的函数都是calculation类型的函数,例如下面的add和sub是calculation类型。
func add(x, y int) int { |
add和sub都能赋值给calculation类型的变量。
var c calculation |
函数类型变量
我们可以声明函数类型的变量并且为该变量赋值:
func main() { |
8. 高阶函数
高阶函数分为函数作为参数和函数作为返回值两部分。
函数作为参数
函数可以作为参数:
func add(x, y int) int { |
函数作为返回值
函数也可以作为返回值:
func do(s string, , add func(x, y int) int, sub func(x, y int) int) (func(int, int) int, error) { |
9. 匿名函数和闭包
匿名函数
Go语言支持匿名函数,即在需要使用函数时再定义函数。
匿名函数没有函数名只有函数体,可以作为一种类型被赋值给函数类型的变量,匿名函数也可以变量方式传递。
匿名函数的用途非常广泛,它本身就是一种值,可以方便地保存在各种容器中实现回调函数和操作封装。
定义
函数当然还可以作为返回值,但是在Go语言中函数内部不能再像之前那样定义函数了,只能定义匿名函数。匿名函数就是没有函数名的函数,匿名函数的定义格式如下:
func(参数列表)(返回参数列表){ |
匿名函数因为没有函数名,所以没办法像普通函数那样调用,所以匿名函数需要保存到某个变量或者作为立即执行函数:
func main() { |
匿名函数多用于实现回调函数和闭包。
调用
在定义时调用匿名函数
func(data int) { |
将匿名函数赋值给变量
// 将匿名函数体保存到f()中 |
用作回调函数
func visit(list []int, f func(int)) { |
闭包
介绍
闭包指的是一个函数和与其相关的引用环境组合而成的实体。简单来说,闭包=函数+引用环境
。
- Go语言中闭包是引用了自由变量的函数,被引用的自由变量和函数一同存在,即使已经离开了自由变量的环境也不会被释放或者删除,在闭包中可以继续使用这个自由变量,因此,简单的说:
函数 + 引用环境 = 闭包
- 一个函数类型就像结构体一样,可以被实例化,函数本身不存储任何信息,只有与引用环境结合后形成的闭包才具有“记忆性”,函数是编译期静态的概念,而闭包是运行期动态的概念。
其它编程语言中的闭包
闭包(Closure)在某些编程语言中也被称为 Lambda 表达式。
闭包对环境中变量的引用过程也可以被称为“捕获”,在 C++11 标准中,捕获有两种类型,分别是引用和复制,可以改变引用的原值叫做“引用捕获”,捕获的过程值被复制到闭包中使用叫做“复制捕获”。
在 Lua 语言中,将被捕获的变量起了一个名字叫做 Upvalue,因为捕获过程总是对闭包上方定义过的自由变量进行引用。
闭包在各种语言中的实现也是不尽相同的,在 Lua 语言中,无论闭包还是函数都属于 Prototype 概念,被捕获的变量以 Upvalue 的形式引用到闭包中。
C++ 与 C# 中为闭包创建了一个类,而被捕获的变量在编译时放到类中的成员中,闭包在访问被捕获的变量时,实际上访问的是闭包隐藏类的成员。
示例
func adder() func(int) int { |
变量f
是一个函数并且它引用了其外部作用域中的x
变量,此时f
就是一个闭包。 在f
的生命周期内,变量x
也一直有效。 闭包进阶示例1:
func adder2(x int) func(int) int { |
闭包进阶示例2:
func makeSuffixFunc(suffix string) func(string) string { |
闭包进阶示例3:
func calc(base int) (func(int) int, func(int) int) { |
闭包其实并不复杂,只要牢记闭包=函数+引用环境
。
闭包内部修改引用的变量
闭包对它作用域上部的变量可以进行修改,修改引用的变量会对变量进行实际修改:
// 准备一个字符串 |
记忆效应
被捕获到闭包中的变量让闭包本身拥有了记忆效应,闭包中的逻辑可以修改闭包捕获的变量,变量会跟随闭包生命期一直存在,闭包本身就如同变量一样拥有了记忆效应。
package main |
- 对比输出的日志发现 accumulator 与 accumulator2 输出的函数地址不同,因此它们是两个不同的闭包实例。
- 每调用一次 accumulator 都会自动对引用的变量进行累加。
工程模式
闭包的记忆效应被用于实现类似于设计模式中工厂模式的生成器,下面的例子展示了创建一个玩家生成器的过程。
package main |
闭包还具有一定的封装性,第 8 行的变量是 playerGen 的局部变量,playerGen 的外部无法直接访问及修改这个变量,这种特性也与面向对象中强调的封装性类似。
10. 递归函数
递归,就是在运行过程中调用自己。
介绍
- 所谓递归函数指的是在函数内部调用函数自身的函数。
- 从数学解题思路来说,递归就是把一个大问题拆分成多个小问题,再各个击破。实际开发过程中,递归函数可以解决许多数学问题,如计算给定数字阶乘、产生斐波系列等。
条件
构成递归需要具备以下条件:
一个问题可以被拆分成多个子问题;
拆分前的原问题与拆分后的子问题除了数据规模不同,但处理问题的思路是一样的;
不能无限制的调用本身,子问题需要有退出递归状态的终止条件。否则就会无限调用下去,直到内存溢出。
例子
1. 斐波那契数列
以递归函数的经典示例 —— 斐波那契数列为例,数列的形式如下所示:
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181, 6765, 10946, … |
实现:
func main() { |
输出:
数列第 1 位: 1 |
2. 数字阶乘
一个正整数的阶乘(factorial)是所有小于及等于该数的正整数的积,并且 0 的阶乘为 1,自然数 n 的阶乘写作n!
,“基斯顿·卡曼”在 1808 年发明了n!
这个运算符号。
例如,n!=1×2×3×…×n
,阶乘亦可以递归方式定义:0!=1,n!=(n-1)!×n
。
func Factorial(n uint64) (result uint64) { |
内存缓存
- 递归函数的缺点就是比较消耗内存,而且效率比较低,性能比较低。
- 当在进行大量计算的时候,提升性能最直接有效的一种方式是避免重复计算,通过在内存中缓存并重复利用缓存从而避免重复执行相同计算的方式称为内存缓存。
const LIM = 41 |
输出如下:
数列第 1 位: 1 |
通过运行结果可以看出,同样获取数列第 40 位的数字,使用内存缓存后所用的时间为 0.0149603 秒,对比之前未使用内存缓存时的执行效率,可见内存缓存的优势还是相当明显的。
11. defer 延迟执行
介绍
Go语言中的defer
语句会将其后面跟随的语句进行延迟处理。在defer
归属的函数即将返回时,将延迟处理的语句按defer
定义的逆序进行执行,也就是说,先被defer
的语句最后被执行,最后被defer
的语句,最先被执行。
规则:未被defer的语句按序先执行;等待未defer执行完毕,被defer的语句逆序执行。
- Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数运行完返回语句时,将延迟处理的语句按后进先出执行。
- 关键字 defer 的用法类似于面向对象编程语言 Java 和 C# 的 finally 语句块,它一般用于释放某些已分配的资源,典型的例子就是对一个互斥解锁,或者关闭一个文件。
例子:
func main() { |
输出结果:
start |
由于defer
语句延迟调用的特性,所以defer
语句能非常方便的处理资源释放问题。比如:资源清理、文件关闭、解锁及记录时间等。
defer执行时机
在Go语言的函数中return
语句在底层并不是原子操作(一步完成操作),它分为给返回值赋值和RET指令两步。而defer
语句执行的时机就在返回值赋值操作后,RET指令执行前。具体如下图所示:
释放资源
释放锁
var ( |
释放文件句柄
func fileSize(filename string) int64 { |
参考感谢
函数 · 语雀 (yuque.com)
05.函数 · 语雀 (yuque.com)
函数 · 语雀 (yuque.com)
Go语言基础之函数 · 语雀 (yuque.com)
函数 · 语雀 (yuque.com)
func 函数 · 语雀 (yuque.com)