【Go】Go数据类型详解—函数
1. 前言
也许很多编程语言都不会把函数归类为数据类型,但是在Go语言当中函数就是一种数据类型(关于这个话题在后面章节会具体阐述)我们现在要理解的一个重要问题就是:为什么需要引入函数?——我们现在已经拥有了bool、int、string等基本数据类型、也拥有if、for等分支循环语句,不是已经可以完成任何的功能了吗?现在来看这样一个需求:
1.1 没有函数带来的问题
📖 需求:在主函数当中完成以下三个步骤:1、打印当前二维数组 2、将二维数组第一个元素置为1 3、打印当前二维数组
功能代码如下:
func main() {
var arr [5][5]int
// 打印二维数组
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
fmt.Printf("%d ", arr[i][j])
}
fmt.Println()
}
// 将第一个元素置为1
arr[0][0] = 1
// 打印二维数组
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
fmt.Printf("%d ", arr[i][j])
}
fmt.Println()
}
}
相信大家都可以看懂上一段代码的含义,功能的确可以正常实现,但是出现了以下两个问题:
- 代码重复:打印数组的功能出现了两次,并且代码是重复的,那么如果需要修改就需要级联修改多处内容。可扩展性、可维护性不高
- 耦合性强:在main方法中将打印功能和逻辑修改功能紧密结合在一起,如果main中出现问题就要同时排查这两个功能代码块
1.2 初窥函数
现在就引入函数来解决该问题:
// 打印二维数组
func printArr(arr [5][5]int) {
for i := 0; i < 5; i++ {
for j := 0; j < 5; j++ {
fmt.Printf("%d ", arr[i][j])
}
fmt.Println()
}
}
func main() {
var arr [5][5]int
// 打印二维数组
printArr(arr)
// 将第一个元素置为1
arr[0][0] = 1
// 打印二维数组
printArr(arr)
}
上述代码依旧能够正常实现功能(看不懂没关系)最重要的一点就是我们已经明白了函数的作用:去重解耦!
2. 函数声明和调用
2.1 函数声明
函数声明使用func关键字,语法格式如下:
func 函数名(形参列表...) (返回值列表...) {
函数体
return 返回值
}
- 函数名:与变量名规则相同,由字母、数字、下划线组成,且不能以数字开头
- 形式参数列表:由形式参数名和形式参数类型组成,多个参数之间使用","分隔
- 返回值列表:由返回值变量以及变量类型组成,也可以只有变量类型,多个返回值必须用"()"包裹
- 函数体:函数具体执行的代码块
比如下面这个函数的功能就是用来打印1-100的累加和:
func printSum1To100() {
var sum = 0
for i := 1; i <= 100; i++ {
sum += i
}
fmt.Println(sum)
}
2.2 函数调用
函数声明仅仅起到了一个将功能代码块组织起来的方式,仅仅声明一个函数并不会执行其中的代码,想要执行还得通过函数的调用:
函数调用格式如下:函数名(实际参数列表)
上述代码调用格式如下:
func main() {
printSum1To100()
}
通过调用,函数体的代码才会正确执行!
3. 函数参数
3.1 形参与实参
函数参数实际上就是为了提高可扩展性而设计的:上述函数唯一的用途就是打印1-100的和,那如果我现在想要打印2-1000的和难道还得另外编写一个函数吗?函数参数就提供了一种调用者-执行者之间传递值的机制:现在我们要求函数能够打印[a, b]区间的和(a, b由调用者指定)代码修改如下:
func printSumFromAToB(a int, b int) {
var sum = 0
for i := a; i <= b; i++ {
sum += i
}
fmt.Println(sum)
}
func main() {
printSumFromAToB(1, 100)
printSumFromAToB(10, 1000)
}
其中函数声明处的参数称为形式参数,函数调用中的参数被称为实际参数,函数调用过程中存在一个值拷贝的行为,即会将 实际参数的值拷贝给形式参数 ,有了函数参数,程序的功能性和可扩展性就大大提高了!
3.2 可变参数
如果想要让一个函数能够接受任意多个值或者不确定函数参数有多少个值时就可以使用可变参数(也叫不定长参数),只需要在声明位置变量名之后+"…"即可,语法格式:func calSum(nums ...int) {}
如下列函数的功能就是进行对任意多个数的求和:
func calSum(nums ...int) {
fmt.Println(reflect.TypeOf(nums))
var sum = 0
for _, num := range nums {
sum += num
}
fmt.Println(sum)
}
func main() {
calSum(1, 5, 10, 15)
}
程序运行结果如下:
⭐ 扩展知识1:我们通过reflect.TypeOf方法获取到了nums对应的类型是一个切片,本质上是Go的语法糖将实参传递的值都放进了一个切片中
⭐ 扩展知识2:当可变参数与普通参数一起出现时,需要将可变参数的位置放置于普通参数之后!例如:func calSum(a, int, b int, nums …int)
4. 函数返回值
4.1 返回值基本使用
返回值的作用:函数返回值可以将函数体当中某一个值在调用完毕后返回给主调函数使用,比如在上述案例中打印了1-100的和,但是我希望不要在被调函数内部直接打印,而是将选择权转交给main函数进行打印
函数声明格式:func 函数名(形参列表...) (返回值列表...) {return 返回值}
下面我们重新修改成带函数返回值的版本代码:
func calSumFromAToB(a int, b int) int {
var sum = 0
for i := a; i <= b; i++ {
sum += i
}
return sum
}
func main() {
var sum = calSumFromAToB(1, 100)
fmt.Println(sum)
}
上述代码中calSumFromAToB
函数将内部sum变量赋值给了主调函数中的sum变量完成拷贝过程!
4.2 无返回值
当声明函数时没有显示声明函数返回值类型,此时代表该函数功能调用完毕后无返回值(不可在主调函数中进行接收)
func calSum(num1 int, num2 int) {
var sum = num1 + num2
fmt.Println(sum)
}
func main() {
// 无返回值不可进行接收
// var sum = calSum(1, 2) // 语法错误
}
4.3 返回多个值
与其他主流语言C、Java不同,Go语言当中的函数也可以返回多个值:比如下面getInfo
函数同时返回了name和age两个变量的值
func getInfo() (string, int) {
return "zhangsan", 18
}
func main() {
name, age := getInfo()
fmt.Println(name, age)
}
💡 小贴士:在Go语言当中,如果函数返回多个值当中有不需要使用的值,可以使用下划线匿名变量"_"进行接收,比如此处年龄不需要使用就可以改成 name, _ := getInfo()
4.4 返回值命名
在函数定义过程中,还可以提前声明返回值变量,最后直接使用return关键字返回,默认返回的就是定义的返回值变量,例如:
func calSumFromAToB(a int, b int) (sum int) {
for i := a; i <= b; i++ {
sum += i
}
return // 默认返回sum的值
}
func main() {
var sum = calSumFromAToB(1, 10)
fmt.Println(sum)
}
5. 匿名函数
匿名函数:顾名思义就是没有名字的函数,其声明格式如下:
声明格式:func (形参列表...)(返回值列表...){}
应用场景:在某些情况下函数当中的功能仅仅需要调用一次或仅在当前函数中使用,此时如果将函数定义在全局位置会消耗性能甚至存在全局污染问题,Go语言提供了一种机制能够在函数内部定义函数:即匿名函数
- 匿名函数使用方式1:直接声明并调用
func main() {
// 声明匿名函数并直接调用
(func(a int, b int) {
fmt.Println(a + b)
})(1, 2)
}
- 匿名函数使用方式2;使用变量赋值并调用
func main() {
// 使用变量接收匿名函数
var f = func(a int, b int) {
fmt.Println(a + b)
}
f(1, 2)
fmt.Println(reflect.TypeOf(f))
}
执行结果如下:
❗ 注意:我们使用TypeOf函数打印 f 的类型,发现是 func(int, int)类型,足以看出函数在Go语言当中也是一种数据类型!
6. 高阶函数
高阶函数:高阶函数本质就是一个函数,满足下面任一条件的就称之为高阶函数:
- 使用函数作为一个函数的参数
- 使用函数作为一个函数的返回值
6.1 使用函数作为参数
代码示例:
// 高阶函数
func f1(f func()) {
f()
}
func main() {
var f = func() {
fmt.Println("匿名函数被调用...")
}
f1(f)
}
代码执行结果如下:
❓ 易混点:在全局定义了一个函数 f1 ,其中 f1 的参数是一个函数,满足了高阶函数的条件1,因此f1就是一个高阶函数!在 main 函数中我们定义了一个匿名函数赋值给了变量 f,调用 f1 函数并传递参数f,在 f1 函数中进行调用 f 函数完成打印
6.2 使用函数作为返回值
代码示例:
// 高阶函数
func f2() func() {
var f = func() {
fmt.Println("匿名函数被调用...")
}
return f
}
func main() {
var f = f2()
f()
}
代码执行结果如下:
❓ 易混点:在全局定义了一个函数 f2 ,其中 f2 的返回值是一个函数,满足了高阶函数的条件2,因此f2就是一个高阶函数!在 f2 函数内部定义了一个匿名函数并赋值给变量f,然后进行返回。在main函数中调用 f2 函数接收到了返回值(为一个函数),进行调用完成打印
7. 闭包函数
闭包:闭包在编程领域是一个广泛使用的概念,在维基上对闭包的解释就是为"闭包也被称为词法闭包或者函数闭包,是引用的自由变量(外部非全局)的函数"
单看定义也许还是有点抽象,别着急,从案例入手来体会闭包的含义就简单多了!
7.1 闭包示例
先来看一个需求:实现一个计数器功能,每次调用计数器函数,计数值+1
我们可以使用全局变量来实现这个功能:
var count = 0
func counter() {
count++
fmt.Println(count)
}
func main() {
counter()
counter()
counter()
}
代码执行结果如下:
分析:使用全局变量当然可以解决这个问题,但是上述代码存在一个致命缺陷:可能存在别的函数会污染了全局变量 count!
解决方案:我们现在希望将count与counter组织在一起,我们只学过函数这一种代码组织形式,但是函数内部无法显式声明一个函数,只能通过匿名函数的方式,代码如下:
func getCounter() func() {
var count = 0
var counter = func() {
count++
fmt.Println(count)
}
return counter
}
func main() {
var counter = getCounter()
counter()
counter()
counter()
}
代码执行结果如下:
由于上述代码中counter函数引用了变量count(外部非全局),因此闭包俨然形成!
❓ 易错点:有些了解作用域的同学可能会疑惑:当getCounter函数调用结束后,局部变量count不是会销毁吗?那么调用counter函数过程中使用count变量不是应该会报错么,事实上存在着作用域提升的环节,由于getCounter返回了一个闭包函数(引用了count变量)此时count作用域伴随counter函数消亡而消亡!!!
7.2 闭包函数应用案例
使用闭包函数还可以实现一个"装饰器函数"的功能:下面这段代码可以计算一个函数的运行时间
func foo() {
fmt.Println("foo功能开始...")
time.Sleep(2 * time.Second)
fmt.Println("foo功能结束...")
}
func bar() {
fmt.Println("bar功能开始...")
time.Sleep(3 * time.Second)
fmt.Println("bar功能结束...")
}
func getTimer(f func()) func() {
return func() {
var startTime = time.Now().Unix()
f()
var endTime = time.Now().Unix()
fmt.Printf("函数执行时长: %d秒\n", endTime-startTime)
}
}
func main() {
foo := getTimer(foo)
foo()
bar := getTimer(bar)
bar()
}
代码执行结果如下:
8. defer语句
defer:它在Go语言当中是一个关键字,是一种注册延迟调用的机制
8.1 defer基本用法
当在一个函数中使用defer语句时,这个语句不会立即执行,而是会等到函数return结束的时候才执行,常用于进行文件、数据库等资源的关闭
func main() {
fmt.Println("1")
defer fmt.Println("2")
fmt.Println("3")
}
代码运行结果:
8.2 多个defer语句执行顺序
当在代码中遇到多个defer语句进行注册延迟调用时,遵循以下原则:先注册的后执行
func main() {
defer fmt.Println(1)
defer fmt.Println(2)
fmt.Println(3)
defer fmt.Println(4)
}
代码执行结果:
8.3 defer的拷贝机制
当使用defer关键字对一个函数进行注册延迟调用时,不会立即调用,会先将函数体以及对应实际参数的拷贝一份放到栈中保存,知道最后函数终止才会执行
8.3.1 案例一
// 案例1
func main() {
foo := func() {
fmt.Println("I am function foo1")
}
defer foo()
foo = func() {
fmt.Println("I am function foo2")
}
}
代码运行结果:
分析:执行语句defer foo()
已经将对应的函数体代码拷贝到defer栈中保存,后续的更改foo变量已经不再产生影响了
8.3.2 案例二
// 案例2
func main() {
x := 10
defer func(a int) {
fmt.Println(a)
}(x)
x++
}
代码运行结果:
分析:执行defer语句已经将对应的实际参数x=10拷贝到defer栈中保存,后续的更改x变量已经不再产生影响了
8.3.3 案例三
// 案例3
func main() {
x := 10
defer func() {
fmt.Println(x) // 保留x的地址
}()
x++
}
代码运行结果:
分析:与上述代码不同的是,defer对应的函数是一个闭包函数,引用的是一个自由变量,因此后面对x的修改会影响到最终打印的x值
8.4 defer的执行时机
在Go语言当中的return实际并不是一个原子操作,而是被拆分成了两步骤:
- rval = xxx
- ret rval
加入defer语句后就变成了三步骤
- rval = xxx
- defer语句执行
- ret
8.5 经典面试题
8.5.1 面试题1
func f1() int {
i := 5
defer func() {
i++
}()
return i
}
func main() {
fmt.Println(f1())
}
代码运行结果:
8.5.2 面试题2
func f2() *int {
i := 5
defer func() {
i++
fmt.Printf(":::%p\n", &i)
}()
fmt.Printf(":::%p\n", &i)
return &i
}
func main() {
fmt.Println(*f2())
}
代码运行结果:
8.5.3 面试题3
func f3() (result int) {
defer func() {
result++
}()
return 5 // result = 5;ret result(result替换了rval)
}
func main() {
fmt.Println(f3())
}
代码运行结果:
8.5.4 面试题4
func f4() (result int) {
defer func() {
result++
}()
return result // ret result变量的值
}
func main() {
fmt.Println(f4())
}
代码运行结果:
8.5.5 面试题5
func f5() (r int) {
t := 5
defer func() {
t = t + 1
}()
return t // ret r = 5 (拷贝t的值5赋值给r)
}
func main() {
fmt.Println(f5())
}
代码运行结果:
8.5.6 面试题6
func f6() (r int) {
fmt.Println(&r)
defer func(r int) {
r = r + 1
fmt.Println(&r)
}(r)
return 5
}
func main() {
fmt.Println(f6())
}
代码运行结果:
8.5.7 面试题7
func f7() (r int) {
defer func(x int) {
r = x + 1
}(r)
return 5
}
func main() {
fmt.Println(f7())
}
代码运行结果: