关于“函数”,相信大家已经不陌生了,我们已经使用两讲的篇幅来介绍它了。在前两讲中,所有的函数都有“名字”,也就是函数名。通常在定义一个函数的时候便会给定函数名,本讲将向各位介绍“匿名函数”。
顾名思义,匿名函数就是没有函数名的函数,这样的函数如何调用呢?在实际开发中的真实使用场景又是怎样的呢?
# 适用场景
我们先举一个日常工作中的例子。在收尾一天的工作时,通常会包含工作状态小结,其中必不可少的便是工作进度。比如:
- 产品UI设计:完成度100%;
- 详细设计文档:完成度50%;
- ……
又比如,当某项工作完结后,虽然没到一天的结束,我们也有可能会向上级做汇报。验证已完成的工作成果,以便继续开展后续的工作。
像这样通过触发器(如每隔一段时间、某个工作获得了阶段性的进展等)执行某个操作(如汇报进度、告知完成情况等)的场景在实际开发中屡见不鲜。
比如执行网络请求,当一个网络请求发出后,程序便“静候佳音”了。当收到网络响应后,会根据响应的具体结果执行后续的操作。
又比如执行本地大量的文件复制操作,我们通常会显示一个进度条,用来方便用户了解实时的复制进度。当操作完成的百分比增加时,进度条也应有对应的增加。
有些朋友可能会有这样的疑问,想要了解进度或完成的情况,直接问不是也行吗?我的回答是:当然可以啦,但这样做并不是最优解。
举个例子,假如我是XX项目的领导,分配了员工A做任务Z。我自认为这项任务很难,估计要一周才能搞定。于是我在周一分配了任务,周五去检查进度,发现任务顺利完成了,这样似乎也没什么问题。
但如果员工A只用了一天就完成了工作呢?由于员工A没有及时汇报工作进展,导致浪费了周二到周五的人力。又如果员工A的能力欠佳,到了周五竟然只完成了30%,这样便会影响到后面的工作安排了。所以我们需要员工A要在每天工作结束时做个日报。
来到实际开发中也是类似的,比如要下载一个文件。我们通常会每隔一段固定的时间查询一次下载进度,并显示在用户界面中。同时也会设计一个“汇报”机制,无论下载完成还是暂停或是失败,都是实时汇报的,这样做便能使下载状态正确且及时地呈现给用户了。
如果少了这个机制,试想假如每隔5秒查询一次下载进度,下载从12:00:00开始,实际在12:00:01就结束。而查询下载进度的逻辑要在12:00:05才能执行,由于12:00:01至12:00:04之间的下载状态更新不及时而导致显示状态出错。
这还不是最麻烦的,麻烦的是如果用户在这个时间段尝试暂停或取消下载,程序该如何应对呢?
像其它的编程语言类似,为了实现这种“汇报”机制,Go语言设计了一种编程方式——“回调”。回调保证了程序运行的正确性和及时性。匿名函数则是实现回调的核心技能。
# 匿名函数的定义和调用
在Go语言中,匿名函数的定义格式如下:
go
复制代码func ([params_list])([return_values_list]){
// 函数体
}
其中,params_list表示参数列表;return_values_list表示函数的返回值列表;使用大括号包裹起来的部分称为函数体,是匿名函数内部要执行的代码。其中,参数列表和返回值列表是可选的。有些函数无需参数,有些参数运行后并不会有任何返回值,有些函数则无需参数也无需返回值。
❗️ 注意: 请大家注意普通函数与匿名函数在定义时的区别,普通函数在定义时仅比匿名函数多了函数名。
定义了函数后,接下来便是如何调用它。根据使用时机的不同,Go语言提供了两种调用匿名函数的方式:一是在定义时调用;二是将匿名函数赋值给变量,通过变量调用。
举例来说,下面的代码定义了一个匿名函数,实际作用便是在控制台输出传入的参数,类型是string:
go
复制代码func main() {
// 定义匿名函数
func(text string) {
fmt.Println(text)
}
}
💡 提示:注意到了吗?和普通函数不同,匿名函数可以在某个普通函数内定义和使用。
如果要在该函数定义时便调用它,只需在大括号结束后,使用小括号将要传入的参数值包裹起来即可,比如:
go
复制代码func main() {
// 定义匿名函数
func(text string) {
fmt.Println(text)
}("定义时就调用")
}
这段代码中,“定义时就调用”便是要传入的参数了。运行这段代码,控制台将输出这些文字。
另一种调用匿名函数的方法是将匿名函数赋值给某个变量,然后通过变量调用。这听起来很神奇,写起来其实非常简单:
go
复制代码func main() {
// 定义匿名函数
exampleVal := func(text string) {
fmt.Println(text)
}
exampleVal("通过变量调用匿名函数")
}
如上代码所示,声明了变量exampleVal,并将匿名函数赋值给了它。在后续的代码中,即可随时使用exampleVal变量调用匿名函数了。
在了解完匿名函数的定义和调用后,我们进行一次实操,模拟网络文件下载,定期查询进度、及时汇报状态。这部分实操内容将会用到第十讲 (opens new window)的内容,如果您还没有充分掌握,建议您先回看,再学习本讲后续的内容。
# 实战回调
对于一个复杂的问题,我们可以将其拆分成多个小问题,然后逐个击破。现在,对于要实现的模拟网络下载的问题,根据实际需要,可以将其分解为两个小问题,这两个小问题分别是:主动定期查询进度和被动接收汇报状态。接下来我们逐个实现它们。
# 主动定期查询进度
本例中,我们将使用一个int型的变量表示进度,初始值为0。它会随着下载进度的增加而增加,直到100,表示下载完成。
此外,我们还会实现一个函数,这个函数的作用就是获取进度,实际上就是将上述变量作为返回值。
因此,这部分代码实现起来较为简单,具体如下:
go
复制代码var percent = 0
func main() {
var keepChecking = true
//开启检查下载进度
for {
if keepChecking {
time.Sleep(500 * time.Millisecond)
fmt.Println("当前进度:", getPercent())
} else {
break
}
}
}
// 获取进度
func getPercent() int {
return percent
}
如上代码所示,程序运行后,便会进入一个没有终止条件的for循环结构中。keepChecking是布尔类型变量,该变量在下载进行时为true,未开始下载、下载取消、下载完成或下载失败时均为false。当该变量为true时,每隔500ms(即0.5秒)调用一次getPercent()函数,向控制台输出一次进度。当该变量为false时,执行break跳出for循环,程序结束。
显然,这是在main()函数中“主动地”查询进度情况。
💡 提示:time.Sleep()表示间隔一段时间执行。本例中,向这个函数传入了500*time.Millisecond,表示500毫秒,即0.5秒的时间。
# 下载并执行回调
继续实现模拟下载函数。我们使用每隔1秒钟,下载进度增加1%的方式来模拟真实的网络下载。进度仍然使用全局有效的percent变量来表示,当该变量增加到100时,下载完成。具体代码如下:
go
复制代码// 下载
func download(url string) {
for {
time.Sleep(1 * time.Second)
percent++
if percent >= 100 {
break
}
}
}
接着,在main()函数中调用它。这里需要使用到“多线程”的概念,我们会在后面的课程中对线程做专题讲解,这里只需理解:多线程就是同时做两件或更多的事。开启线程的方法是在调用某个函数前加上go前缀,具体如下:
go
复制代码func main() {
var keepChecking = true
// 开启下载
fmt.Println("开始下载任务!")
go download("")
//开启检查下载进度
for {
if keepChecking {
time.Sleep(500 * time.Millisecond)
fmt.Println("当前进度:", getPercent())
} else {
break
}
}
}
这段代码中的第5行便是开启了另外一个线程执行download()函数,这样一来,检查下载进度和下载文件便可同时执行了。
如果现在运行程序,将会看到控制台中每隔0.5秒就输出一次下载进度,且进度缓慢增加,到达100后不变。但每隔0.5秒就输出一次的行为并不会被终止,因为keepChecking的值为true。
💡 提示:为节省调试时间,可以尝试先将下载完成的进度定为10或者更少,在测试没问题后再改回100.
为了使下载正常结束,最好的解决方案便是使用回调。在介绍匿名函数的定义和调用时,曾介绍过一种调用的方式是将匿名函数赋值给一个变量,然后通过变量调用这个函数。
既然匿名函数可以赋值给变量,而变量又可以在函数间传递,因此,我们便可以在main()函数中实现一个匿名函数,作用就是将keepChecking置为true,然后向download()函数中传入这个匿名函数。当下载进度抵达100后,在download()函数中通过传入的变量调用匿名函数即可实现实时状态“汇报”,即回调的编程方式了。
将download()函数稍加修改,使其能够接受匿名函数变量,并在下载状态发生改变时调用这个变量:
go
复制代码// 下载
func download(url string, downloadSuccess func(), downloadCancelled func(int), downloadFailed func(int)) {
for {
time.Sleep(1 * time.Second)
percent++
if percent == 100 {
downloadSuccess()
break
}
}
}
💡 提示:上面的代码中,除了下载完成拥有对应的匿名函数变量外,还要求函数的调用者传入下载取消和下载失败时的匿名函数变量。这在实际开发中是很必要的,通常我们并非只关心成功下载事件,对于其它状态也会做各自的处理。
接下来,修改main()函数如下:
go
复制代码func main() {
var keepChecking = true
// 开启下载
fmt.Println("开始下载任务!")
go download("", func() {
keepChecking = false
fmt.Println("下载完成!")
}, func(currentPercent int) {
keepChecking = false
fmt.Println("下载取消!", currentPercent)
}, func(currentPercent int) {
keepChecking = false
fmt.Println("下载失败!", currentPercent)
})
//开启检查下载进度
for {
if keepChecking {
time.Sleep(500 * time.Millisecond)
fmt.Println("当前进度:", getPercent())
} else {
break
}
}
}
运行修改后的完整代码,可以发现,一旦下载进度抵达100%,程序便会自动停止运行。
# 思考题
最后,给大家留个动手的思考题。本例模拟了下载完成的情况,请大家尝试模拟当下载到10%时就取消下载以及下载到30%时下载失败的情况。(答案附后)
# 总结
🎉 恭喜,您完成了本次课程的学习!
📌 以下是本次课程的重点内容总结:
- 匿名函数
和普通函数不同,匿名函数便是没有函数名的函数。其定义方法和定义普通函数基本一致,只是省略了函数名。
在调用匿名函数时,可将匿名函数赋值给变量,便于在函数间传递,从而实现回调机制。
回调式编程在实际的开发中屡见不鲜,它确保了程序运行状态的正确性和及时性,在符合类似“当……时,执行……操作”的逻辑中被广泛运用。
➡️ 在下次课程中,我们会继续介绍Go语言中的函数,具体内容是:
- 闭包
# 思考题答案
go
复制代码package main
import (
"fmt"
"time"
)
var percent = 0
func main() {
var keepChecking = true
// 开启下载
fmt.Println("开始下载任务!")
go download("", func() {
keepChecking = false
fmt.Println("下载完成!")
}, func(currentPercent int) {
keepChecking = false
fmt.Println("下载取消!", currentPercent)
}, func(currentPercent int) {
keepChecking = false
fmt.Println("下载失败!", currentPercent)
})
//开启检查下载进度
for {
if keepChecking {
time.Sleep(500 * time.Millisecond)
fmt.Println("当前进度:", getPercent())
} else {
break
}
}
}
// 获取进度
func getPercent() int {
return percent
}
// 下载
func download(url string, downloadSuccess func(), downloadCancelled func(int), downloadFailed func(int)) {
for {
time.Sleep(1 * time.Second)
percent++
if percent == 30 {
downloadFailed(percent)
break
}
if percent == 10 {
downloadCancelled(percent)
break
}
if percent >= 100 {
downloadSuccess()
break
}
}
}