本文是「Go语言100个实战案例 · 网络与并发篇」系列第4篇,带你通过一个实战案例掌握 Channel 的用法,并用它优雅地实现经典并发模式:生产者-消费者模型。
一、前言
在并发编程中,生产者-消费者模型是最基础、最常见的通信模式之一。它广泛应用于日志系统、任务队列、消息传递、数据流管道等场景。
Go 语言对并发编程的支持非常强大,其中最具代表性的就是 Channel —— 一种内置的通信机制,用于在多个 Goroutine 之间安全地传递数据。
“Don't communicate by sharing memory; share memory by communicating.” —— Go 编程哲学
二、目标说明
我们将实现一个完整的生产者-消费者模型,包含以下要点:
- • 使用 Channel 作为通信桥梁
- • 多个生产者并发生成任务
- • 多个消费者并发处理任务
- • 正确关闭 Channel,防止阻塞或 panic
- • 可配置的任务数量和生产者/消费者数量
三、生产者消费者模型图解
+-------------+ +-------------+ | Producer 1 | | Consumer 1 | +-------------+ +-------------+ │ │ +-------------+ +------------------+ | Producer N |──────▶ | Channel | ────▶ Consumer M ... +-------------+ +------------------+
四、完整代码实现
package main import ( "fmt" "math/rand" "sync" "time" ) // 任务结构体 type Task struct { ID int Value int } // 生产者函数 func producer(id int, taskCh chan<- Task, wg *sync.WaitGroup, count int) { defer wg.Done() for i := 0; i < count; i++ { task := Task{ ID: id*1000 + i, Value: rand.Intn(100), } fmt.Printf("[生产者-%d] 生产任务:%v\n", id, task) taskCh <- task time.Sleep(time.Millisecond * time.Duration(rand.Intn(300))) } } // 消费者函数 func consumer(id int, taskCh <-chan Task, wg *sync.WaitGroup) { defer wg.Done() for task := range taskCh { fmt.Printf("----[消费者-%d] 消费任务:%v\n", id, task) time.Sleep(time.Millisecond * time.Duration(rand.Intn(500))) } fmt.Printf("----[消费者-%d] 任务完成,退出\n", id) } func main() { rand.Seed(time.Now().UnixNano()) taskCh := make(chan Task, 10) // 带缓冲的通道 producerCount := 3 consumerCount := 2 tasksPerProducer := 5 var wgProducers sync.WaitGroup var wgConsumers sync.WaitGroup // 启动消费者 for i := 1; i <= consumerCount; i++ { wgConsumers.Add(1) go consumer(i, taskCh, &wgConsumers) } // 启动生产者 for i := 1; i <= producerCount; i++ { wgProducers.Add(1) go producer(i, taskCh, &wgProducers, tasksPerProducer) } // 等所有生产者完成后关闭通道 go func() { wgProducers.Wait() close(taskCh) }() // 等待所有消费者完成 wgConsumers.Wait() fmt.Println("所有任务已处理完毕。程序退出。") }
五、运行效果示例
运行结果(部分):
[生产者-1] 生产任务:{1000 84} ----[消费者-1] 消费任务:{1000 84} [生产者-2] 生产任务:{2000 56} [生产者-3] 生产任务:{3000 21} ----[消费者-2] 消费任务:{2000 56} ----[消费者-1] 消费任务:{3000 21} ... ----[消费者-1] 任务完成,退出 ----[消费者-2] 任务完成,退出 所有任务已处理完毕。程序退出。
六、关键知识点讲解
1. chan Task
Channel 是 Go 提供的安全通信机制,可用于传递任何类型的数据。在本例中,我们使用 chan Task 来传递结构体任务。
2. chan<- 与 <-chan
- •
chan<- Task:只写通道,生产者用 - •
<-chan Task:只读通道,消费者用
这是一个非常好的习惯,有助于在编译期限制错误。
3. close(chan)
通道关闭后,消费者仍然可以继续读取未消费的数据。一旦通道为空,读取操作会返回零值并终止迭代。
4. sync.WaitGroup
用于等待多个生产者/消费者完成,是并发控制中非常重要的同步工具。
七、常见错误和注意事项
| 问题描述 | 正确做法 |
| 向关闭的通道写入会 panic | 生产者完成后再关闭通道,不能提前 |
| 读通道未关闭,程序阻塞 | 主程序需要关闭通道以让 range 退出 |
| 使用无缓冲通道导致性能瓶颈 | 推荐使用缓冲通道,避免 goroutine 阻塞太多 |
| 竞争条件导致数据错乱 | Channel 可以有效避免锁竞争,推荐使用 |
八、适用场景扩展
- • 日志采集与异步写入(生产日志,消费写磁盘)
- • 网络爬虫任务调度(种子生产与页面处理)
- • 并发图像/视频转码任务
- • 实时监控告警系统(采集 → 分析 → 通知)
九、总结
在本篇文章中,我们通过 Go 的 Channel 构建了一个完整的生产者-消费者模型。你学会了:
✅ 如何创建 Channel 并传递任务
✅ 如何并发运行多个生产者和消费者
✅ 如何正确关闭通道并防止 panic
✅ 如何通过 WaitGroup 同步主线程生命周期
这正是 Go 并发编程哲学的真实体现:通过通信来共享内存,而不是通过共享内存来通信。