Go语言实战案例:使用channel实现生产者消费者模型

本文是「Go语言100个实战案例 · 网络与并发篇」系列第4篇,带你通过一个实战案例掌握 Channel 的用法,并用它优雅地实现经典并发模式:生产者-消费者模型。


一、前言

在并发编程中,生产者-消费者模型是最基础、最常见的通信模式之一。它广泛应用于日志系统、任务队列、消息传递、数据流管道等场景。

Go 语言对并发编程的支持非常强大,其中最具代表性的就是 Channel ------ 一种内置的通信机制,用于在多个 Goroutine 之间安全地传递数据

"Don't communicate by sharing memory; share memory by communicating." ------ Go 编程哲学


二、目标说明

我们将实现一个完整的生产者-消费者模型,包含以下要点:

  • • 使用 Channel 作为通信桥梁
  • • 多个生产者并发生成任务
  • • 多个消费者并发处理任务
  • • 正确关闭 Channel,防止阻塞或 panic
  • • 可配置的任务数量和生产者/消费者数量

三、生产者消费者模型图解

diff 复制代码
+-------------+             +-------------+
|  Producer 1 |             | Consumer 1  |
+-------------+             +-------------+
        │                         │
+-------------+         +------------------+
|  Producer N |──────▶  |     Channel      | ────▶ Consumer M ...
+-------------+         +------------------+

四、完整代码实现

go 复制代码
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("所有任务已处理完毕。程序退出。")
}

五、运行效果示例

运行结果(部分):

css 复制代码
[生产者-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 并发编程哲学的真实体现:通过通信来共享内存,而不是通过共享内存来通信


相关推荐
回家路上绕了弯1 分钟前
为什么 AI 写代码时,总喜欢“防御性编程”?
后端
灯澜忆梦4 分钟前
【Redis中间件】#4 | 进阶数据类型语法
redis·后端
广州山泉婚姻7 分钟前
前后端分离的核心优势
前端·后端
IT_陈寒26 分钟前
Vue的响应式让我加班到凌晨,问题竟出在这个不起眼的地方
前端·人工智能·后端
j7~27 分钟前
【C++微服务项目开发脚手架】(准备篇)从 0 到跑通:Docker 容器开发环境 + 宿主机用户 + Trae 远程连接 全链路踩坑实战
linux·c++·学习·xshell·docker容器·trae
乱码三千38 分钟前
使用 Docker-compose 搭建SRS直播中转服务端
后端·架构·github
y1su38 分钟前
【Leetcode】1477. 找两个和为目标值且不重叠的子数组
数据结构·后端·算法·leetcode·职场和发展
愛芳芳1 小时前
Swagger2 多环境动态配置实战指南
java·后端·maven·springboot·swagger
JaguarJack2 小时前
PHP 8.6 只是个小版本,三十项弃用却已在为 PHP 9.0 铺路
后端·php·服务端
BingoGo2 小时前
PHP 8.6 只是个小版本,三十项弃用却已在为 PHP 9.0 铺路
后端·php