练习
学完本章你应该掌握
- 能使用
go启动 goroutine,理解主 goroutine 不会自动等待其他任务,并能判断为什么并发输出顺序不固定。 - 能正确使用
sync.WaitGroup的Add、Done、Wait协调一组任务结束,并识别计数不匹配、循环变量传递错误等常见问题。 - 能使用
channel在 goroutine 之间传递数据、通知结束,并写对close、range、发送与接收的配合关系。 - 能使用
select同时等待多个 channel 事件,区分阻塞等待、default非阻塞检查和“哪个先准备好就先处理”的行为。 - 能使用
time.After为并发任务增加超时控制,并理解为什么超时场景里常配合带缓冲结果 channel。 - 能把
01-09单元的函数、结构体、切片、map、自定义类型、接口、判断和循环自然放进并发题目里,而不是只会写孤立语法。
简单
第 1 题:并发启动三个学习任务并等待结束
编写一个 Go 程序,完成下面要求:
- 准备一个切片:
| |
- 编写函数
runTask(name string, wg *sync.WaitGroup) - 这个函数输出:
开始:任务名 - 在
main中为每个任务启动一个 goroutine - 使用
sync.WaitGroup等待这 3 个任务都输出完成 - 最后输出:
全部任务已启动完成
说明:前 3 行任务输出顺序不固定,只要最后一行一定出现在全部任务输出之后即可。
查看参考答案
| |
说明:这道题的重点是体会 go 启动任务后,主 goroutine 仍会继续往下走,所以必须用 WaitGroup 明确等待。
第 2 题:用 channel 取回两个数的和
编写一个 Go 程序,完成下面要求:
- 编写函数
calcSum(a, b int, resultCh chan int) - 这个函数把
a + b的结果发送到resultCh - 在
main中创建一个chan int - 使用 goroutine 调用
calcSum(12, 30, resultCh) - 在主 goroutine 中接收结果,并输出:
| |
查看参考答案
| |
说明:这里主 goroutine 不是靠 time.Sleep 猜时间,而是直接在 <-resultCh 处等待结果到来。
第 3 题:修正不会结束的 channel 遍历
下面程序的目标是输出:
| |
但它现在会一直卡住。请改正代码,让它在输出完 1、2、3 后正常结束。
| |
查看参考答案
修正后的代码如下:
| |
原因说明:
for number := range numbers会一直读取,直到numbers被关闭。- 原程序只发送了 3 个值,但没有
close(numbers),所以接收方不知道“数据已经发完了”,就会继续阻塞等待。 - 这里由发送方关闭 channel,更符合这一章的常见写法。
一般
第 4 题:并发生成学生成绩摘要
编写一个 Go 程序,完成下面要求:
- 定义结构体
Student,字段包括:Name stringScore int - 准备下面这组数据:
| |
- 编写函数
buildSummary(student Student) string - 返回格式如下:
如果分数大于等于
60,返回小李: 82 (及格)否则返回小王: 59 (不及格) - 为每个学生启动一个 goroutine 生成摘要,并把摘要发送到
summaryCh - 使用
WaitGroup等待所有 goroutine 完成后关闭summaryCh - 在
main中使用for range读取并输出所有摘要
说明:摘要输出顺序不固定。
查看参考答案
| |
说明:这道题把结构体、函数、判断、切片、goroutine、WaitGroup 和 channel 串起来了,很接近“并发生产结果,主流程统一收集”的基本模型。
第 5 题:谁先返回就先显示哪条消息
编写一个 Go 程序,完成下面要求:
- 创建两个
chan string:articleChexerciseCh - 启动两个 goroutine:
一个在
120ms后向articleCh发送文章:channel 入门一个在60ms后向exerciseCh发送练习:WaitGroup 实战 - 使用
select接收这两条消息 - 总共接收 2 次,并按“谁先准备好就先输出谁”的顺序打印
期望输出类似这样:
| |
查看参考答案
| |
说明:这道题的重点是理解 select 不是按 case 书写顺序执行,而是谁先准备好就先进入谁。
第 6 题:用 select + default 写一个等待提示器
编写一个 Go 程序,完成下面要求:
- 创建
doneCh := make(chan string) - 启动一个 goroutine,在
120ms后向doneCh发送批改完成 - 在主 goroutine 中使用
for {}+select - 如果
doneCh里还没有消息,就输出等待中... - 一旦收到
批改完成,就输出它并结束程序
要求:
- 使用
default分支 - 为了避免 CPU 空转,请在
default里加一点点等待时间
查看参考答案
| |
说明:
default让select在“还没有消息”时也能先执行别的逻辑。- 如果
default里什么都不做,循环会疯狂空转,所以通常要配合time.Sleep或其他阻塞操作。
第 7 题:实现一个带超时的订单确认函数
编写一个 Go 程序,完成下面要求:
- 编写函数
confirmOrder(orderID int, workTime time.Duration) string - 在这个函数里启动一个 goroutine,模拟后台确认订单
- 如果后台任务在
100ms内完成,就返回:
| |
- 如果超过
100ms还没完成,就返回:
| |
要求:
- 使用结果 channel 接收后台 goroutine 的返回值
- 使用
select + time.After做超时控制 - 在
main中至少测试两次: 一次成功 一次超时
查看参考答案
| |
一种可能的输出结果:
| |
说明:这里把结果 channel 写成 make(chan string, 1),是为了让超时后后台 goroutine 仍然有机会把结果放进去,不容易被卡在发送位置。
第 8 题:并发计算切片平方并按原顺序输出
编写一个 Go 程序,完成下面要求:
- 准备一个切片:
| |
- 定义结构体
SquareResult,字段包括:Index intValue int - 为切片中的每个数字启动一个 goroutine,计算平方
- 每个 goroutine 把结果以
SquareResult的形式发送到resultCh - 主 goroutine 收集所有结果,并恢复成原来的顺序
- 最后输出:
| |
要求:
- 使用
WaitGroup - 所有结果发送完成后关闭
resultCh
查看参考答案
| |
说明:并发结果的返回顺序不一定和原切片顺序一样,所以这里额外把下标一起传回来,再由主 goroutine 按 Index 放回去。
进阶
下面 5 题会把 goroutine、WaitGroup、channel、select、超时处理和前面学过的结构体、自定义类型、接口、函数、切片、map 一起用起来,更接近真实的并发程序组织方式。
第 9 题:实现一个双工人任务处理队列
请完成一个稍微完整一点的小场景:
- 定义结构体
Task,字段包括:ID intName string - 准备下面这组任务:
| |
- 创建任务 channel
jobCh和结果 channelresultCh - 编写函数
worker(workerID int, jobCh chan Task, resultCh chan string, wg *sync.WaitGroup) - 启动 2 个 worker goroutine,共同处理同一条任务队列
- 每个 worker 处理完一项任务后,向
resultCh发送类似下面的结果:
| |
- 所有任务发完后关闭
jobCh - 所有 worker 结束后关闭
resultCh - 主 goroutine 使用
for range输出所有处理结果
说明:结果输出顺序不固定。
查看参考答案
| |
说明:这道题体现了 channel 既可以传“结果”,也可以当“任务队列”使用。两个 worker 同时从同一个 jobCh 取任务,是很常见的并发模型。
第 10 题:并发统计任务状态数量
编写一个 Go 程序,完成下面要求:
- 定义自定义类型
TaskStatus,底层类型是int - 定义三个状态常量:
StatusTodoStatusDoingStatusDone - 给
TaskStatus绑定方法Text() string - 定义结构体
Task,字段包括:Name stringStatus TaskStatus - 准备下面这组任务:
| |
- 为每个任务启动一个 goroutine
- 每个 goroutine 把自己的
Status发送到statusCh - 主 goroutine 使用
map[TaskStatus]int统计每种状态的数量 - 最后按下面顺序输出:
| |
查看参考答案
| |
说明:这道题把自定义类型、方法、结构体、切片、map 和并发收集放到了一起。真正要练的不是“统计本身”,而是把多个 goroutine 产出的值安全汇总到主流程。
第 11 题:修正超时后仍可能卡住发送方的程序
下面程序会输出:
| |
但它还存在一个问题:超时之后,后台 goroutine 仍可能卡在发送结果的位置。
请改正代码,让它在保持超时输出的同时,不再让发送方因为结果没人接收而被卡住。
| |
查看参考答案
修正后的代码如下:
| |
原因说明:
- 原来的
resultCh是无缓冲 channel。 - 超时分支先执行后,主 goroutine 不再接收
resultCh,后台 goroutine 到了resultCh <- "success"这一步就可能一直等着。 - 改成
make(chan string, 1)后,就算主 goroutine已经走到超时分支,后台 goroutine 仍可以把一个结果先放进 channel 里,不容易卡死在发送位置。
第 12 题:实现首个搜索结果优先的资源搜索器
请完成一个稍微完整一点的小场景:
- 定义接口
Searcher,要求实现:Search(keyword string) string - 定义两个实现:
ArticleSearcherVideoSearcher - 两个结构体都带一个字段:
Delay time.Duration Search方法里先等待Delay,再返回对应结果文字- 编写函数
firstResult(keyword string, list []Searcher) string - 这个函数要为切片中的每个搜索器启动一个 goroutine
- 使用
select同时等待“最快返回的搜索结果”和“150ms 超时” - 如果先拿到结果,就直接返回该结果
- 如果超时先到,就返回
timeout
要求:
- 在
main中至少测试两组数据: 一组能在超时前拿到结果 一组会超时
查看参考答案
| |
一种可能的输出结果:
| |
说明:
- 这里的重点是“谁先返回就用谁”,而不是把所有结果都等齐。
resultCh使用了len(list)大小的缓冲,这样即使主流程已经拿到第一个结果或进入超时分支,后面慢一点的 goroutine 也不容易被卡在发送上。
第 13 题:实现一个并发成绩分析中心
请完成一个综合练习:
- 定义结构体
Student,字段包括:Name stringScore int - 准备下面这组数据:
| |
- 编写三个函数:
calcTotal(students []Student, ch chan int)calcPassCount(students []Student, ch chan int)findTopStudent(students []Student, ch chan Student) - 分别启动 3 个 goroutine 去计算: 总分 及格人数 最高分学生
- 主 goroutine 使用 3 个不同的 channel 接收结果
- 再用
select循环收集这 3 份结果 - 最后输出:
| |
查看参考答案
| |
说明:这道题把结构体、切片、函数、goroutine、多个 channel 和 select 全串起来了。真正想练的是“把不同类型的并发结果分别算出来,再在主流程统一收集”。