资讯动态

Go零基础实战第2期:Goroutine与Channel

发布时间:2026/10/15 3:38:57 来源:尧图企业网站定制
文章目录引言Goroutine与Channel一、本篇学习目标与准备二、核心概念与实现思路1. 这个程序在模拟什么2. goroutine 是什么3. channel 是什么4. 无缓冲 channel 和有缓冲 channel5. WaitGroup 是什么6. 为什么 wg.Done() 要配合 defer7. 为什么要关闭 jobsCh8. 为什么要关闭 resultsCh9. worker pool 是什么10. 为什么不是 worker 越多越好三、课程实战运行、源码与测试1. 怎么运行核心源码拆解RunWorkerPool源码中的测试TestRunWorkerPoolStopsWhenContextIsCanceled四、实战练习1. 小练习练习 1修改 worker 数量练习 2增加任务练习 3理解关闭 channel练习 4理解 WaitGroup扩展练习 1验证取消后的退出扩展练习 2理解缓冲容量的限制五、常见问题与排错问题 1结果顺序为什么每次都不一样问题 2任务处理完了range 为什么不结束六、与前文的联系和工程应用1. 和第一课的关系七、本篇小结下期预告✍创作者全栈弄潮儿 个人主页全栈弄潮儿的个人主页️ 个人社区欢迎你的加入全栈开发社区 专栏Go零基础实战引言Goroutine与Channel发送邮件、生成报表和同步数据都要耗时如果一个接一个执行总等待时间就是各项任务耗时的叠加。本篇把五个任务交给三个 worker借助 goroutine 并发执行用 channel 分发任务和收集结果再用 WaitGroup 等待任务池结束。一、本篇学习目标与准备区分同步调用与 goroutine 并发调用。理解缓冲、发送、接收和 channel 关闭。掌握 WaitGroup 与任务池退出顺序。课程目录backend-training/Go/02-goroutine-channel。配套示例按课独立组织每个目录都有自己的go.mod使用 Go 1.22 或更高版本。运行和测试前进入该课目录。正文代码包含源码节选与机制示意节选依赖同课的类型和辅助函数完整程序与测试放在配套源码中。二、核心概念与实现思路1. 这个程序在模拟什么这个程序模拟一个“任务队列 多个 worker 并发处理任务”的模型。真实后端里经常会遇到类似场景多个 worker 消费消息队列。批量发送邮件。批量处理订单。批量刷新缓存。批量同步第三方系统数据。如果只有一个 worker任务只能一个一个做。如果有多个 worker多个任务就可以同时做总耗时通常会下降。2.goroutine是什么代码里有goworker(ctx,workerID,jobsCh,resultsCh,wg)前面的go表示启动一个 goroutine。你可以先把 goroutine 理解成Go 里的轻量级并发任务。普通函数调用是同步的worker(ctx,workerID,jobsCh,resultsCh,wg)这表示当前代码会卡在这里等worker执行完才继续往下走。加上go以后goworker(ctx,workerID,jobsCh,resultsCh,wg)这表示启动一个新的 goroutine 去执行worker当前代码继续往下走。3.channel是什么channel 是 goroutine 之间传递数据的管道。代码里有两个 channeljobsCh:make(chanJob)resultsCh:make(chanResult,len(jobs))jobsCh用来传任务jobsCh-job这表示把一个任务发送到jobsCh。worker 从里面拿任务job,ok:-jobsCh这表示从jobsCh接收一个任务。resultsCh用来传结果resultsCh-resultworker 处理完任务后把结果放进去。4. 无缓冲 channel 和有缓冲 channel这行创建的是无缓冲 channeljobsCh:make(chanJob)无缓冲 channel 的特点是发送方和接收方都准备好了数据才能交接成功。可以理解成面对面递东西你递的时候对方也要在接。这行创建的是有缓冲 channelresultsCh:make(chanResult,len(jobs))有缓冲 channel 可以先暂存一些数据。可以理解成桌上有几个空位你可以先把东西放到桌上对方晚点再拿。5.WaitGroup是什么代码里有varwg sync.WaitGroupWaitGroup用来等待一组 goroutine 全部结束。你可以把它理解成一个计数器。启动一个 worker 前wg.Add(1)goworker(ctx,workerID,jobsCh,resultsCh,wg)表示我要多等一个 goroutine。worker 结束时deferwg.Done()表示这个 goroutine 做完了计数减 1。主流程里wg.Wait()表示等所有 worker 都结束再继续往下走。6. 为什么wg.Done()要配合defer代码里是deferwg.Done()意思是worker 函数返回前一定执行wg.Done()。这样无论 worker 是正常结束还是因为 context 取消提前返回都不会忘记告诉WaitGroup我已经结束了。如果忘记调用wg.Done()wg.Wait()可能会一直等下去程序就卡住了。7. 为什么要关闭jobsCh代码里有deferclose(jobsCh)关闭jobsCh的意思是不会再有新任务发进来了。worker 接收任务时写的是job,ok:-jobsChif!ok{return}这里的ok表示 channel 是否还开着ok true成功收到一个任务。ok falsechannel 已经关闭并且没有剩余任务了。所以关闭jobsCh后worker 就知道可以退出了。8. 为什么要关闭resultsCh代码里有wg.Wait()close(resultsCh)注意顺序先wg.Wait()再close(resultsCh)。原因是必须等所有 worker 都结束后才说明不会再有新的结果写入resultsCh。这时候关闭resultsCh下面的循环才能自然结束forresult:rangeresultsCh{resultsappend(results,result)}如果不关闭resultsChrange resultsCh会一直等新数据程序可能卡住。9. worker pool 是什么worker pool 可以理解成固定数量的工人从同一个任务队列里拿任务处理。本篇里results:RunWorkerPool(context.Background(),jobs,3)表示启动 3 个 worker。如果有 5 个任务3 个 worker 会一起抢任务。谁空了谁继续拿下一个任务。这样比一个 worker 一个个处理更快。10. 为什么不是 worker 越多越好worker 多了并发能力会变强但不是越多越好。因为 worker 太多可能导致数据库连接被打满。Redis 被打满。CPU 被打满。内存占用上升。下游接口被压垮。真实后端里worker 数量通常要根据资源限制来设置。比如数据库连接池最多 20 个连接就不要随便开 100 个 worker 同时查库。第三方接口有限流就要控制并发请求数量。CPU 密集型任务worker 数量通常不要远大于 CPU 核数。三、课程实战运行、源码与测试1. 怎么运行在当前目录执行go run.你会看到类似输出处理完成5 个任务总耗时600ms 任务 1 由 worker 1 处理发送欢迎邮件 完成 任务 2 由 worker 2 处理生成用户报表 完成 任务 3 由 worker 3 处理同步 CRM 数据 完成 任务 4 由 worker 3 处理刷新用户缓存 完成 任务 5 由 worker 1 处理写入行为日志 完成每次运行时worker 编号和总耗时可能会有一点变化这是正常的。因为 goroutine 是并发调度的。执行测试gotest./...核心源码拆解RunWorkerPool以下节选自本篇的main.go需要与同课的类型和辅助函数一起使用。funcRunWorkerPool(ctx context.Context,jobs[]Job,workerCountint)[]Result{ifworkerCount0{workerCount1}jobsCh:make(chanJob)resultsCh:make(chanResult,len(jobs))varwg sync.WaitGroupforworkerID:1;workerIDworkerCount;workerID{wg.Add(1)goworker(ctx,workerID,jobsCh,resultsCh,wg)}gofunc(){deferclose(jobsCh)for_,job:rangejobs{select{case-ctx.Done():returncasejobsCh-job:}}}()wg.Wait()close(resultsCh)results:make([]Result,0,len(jobs))forresult:rangeresultsCh{resultsappend(results,result)}returnresults}源码中的测试TestRunWorkerPoolStopsWhenContextIsCanceled测试节选来自同课的测试文件辅助函数也在该文件中。阅读时关注输入条件与断言不只看是否调用了某个函数。funcTestRunWorkerPoolStopsWhenContextIsCanceled(t*testing.T){ctx,cancel:context.WithCancel(context.Background())cancel()jobs:[]Job{{ID:1,Name:job 1,Duration:20*time.Millisecond},{ID:2,Name:job 2,Duration:20*time.Millisecond},}results:RunWorkerPool(ctx,jobs,2)iflen(results)!0{t.Fatalf(expected no results after cancellation, got %d,len(results))}}四、实战练习1. 小练习练习 1修改 worker 数量把results:RunWorkerPool(context.Background(),jobs,3)改成results:RunWorkerPool(context.Background(),jobs,1)再运行go run.观察总耗时有没有变长。然后再改成results:RunWorkerPool(context.Background(),jobs,5)观察总耗时有没有变短。练习 2增加任务在jobs里多加两个任务例如{ID:6,Name:清理临时文件,Duration:250*time.Millisecond},{ID:7,Name:发送运营通知,Duration:350*time.Millisecond},再运行程序观察任务由哪些 worker 处理。练习 3理解关闭 channel临时注释掉deferclose(jobsCh)再运行程序看看会发生什么。思考worker 为什么不知道该退出了练习完以后记得把这行恢复回来。练习 4理解WaitGroup临时注释掉 worker 里的deferwg.Done()再运行程序看看会发生什么。思考主流程为什么一直卡在wg.Wait()练习完以后也记得恢复。扩展练习 1验证取消后的退出创建已经取消的 context调用 RunWorkerPool检查不会继续完成所有任务。扩展为执行过程中取消说明已经完成的结果仍可能被返回。扩展练习 2理解缓冲容量的限制把 resultsCh 的容量改小之前先把结果消费改成与 worker 同时进行。观察只等待 worker、却不读取满缓冲的结果队列为什么可能死锁。五、常见问题与排错问题 1结果顺序为什么每次都不一样并发调度和任务耗时会影响完成顺序。课程按 JobID 排序后展示结果但这不会改变真实执行顺序。测试应验证任务集合而不是依赖 worker 编号。问题 2任务处理完了range 为什么不结束range channel 要等到通道关闭且缓冲数据被读完才退出。生产者需要关闭 jobsCh协调者等所有 worker 退出后再关闭 resultsCh接收方不要提前关闭仍有人发送的通道。六、与前文的联系和工程应用1. 和第一课的关系第一课讲的是context和超时。第二课继续使用context因为并发任务也需要能被取消。比如case-ctx.Done():return表示如果外层请求取消了worker 就不要继续处理任务了。这就是 Go 后端里很重要的思维启动 goroutine 时也要想清楚它什么时候退出。七、本篇小结通过本篇我们完成了区分同步调用与 goroutine 并发调用。理解缓冲、发送、接收和 channel 关闭。掌握 WaitGroup 与任务池退出顺序。请记住这三个结论goroutine 执行任务channel 传递数据WaitGroup 协调结束。通道由能够确定“不会再发送”的一方关闭。并发数应根据下游容量设置不能只追求更多 worker。下期预告下一篇我们将学习Mutex与数据竞争用抢票案例解决库存超卖继续把 Go 的基本机制放进真实后端场景中练习。敬请期待✍坚持原创求关注点赞收藏

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑