Go实现生产者消费者模型的关键在于:channel缓冲大小需权衡内存与吞吐(典型值2–4倍平均产量),close必须由最后一个生产者调用或WaitGroup协调,无缓冲channel易致死锁。

Go 语言实现生产者消费者模型,核心不是“能不能”,而是「channel 缓冲大小、close 时机、WaitGroup 协调点」这三处稍有偏差,程序就可能死锁、panic 或提前退出。
为什么用 make(chan int, N) 而不是 make(chan int)
无缓冲 channel(make(chan int))要求发送和接收必须同时就绪,生产者一发就卡住,直到有消费者 ready —— 这在多 goroutine 场景下极易导致死锁,尤其当消费者启动慢于生产者时。
带缓冲 channel(如 make(chan int, 5))让生产者能先存几条数据再继续,解耦节奏差异。但缓冲区不是越大越好:
- 设太大(比如 10000)会吃内存,且掩盖了消费能力不足的问题
- 设太小(比如 1)虽节省内存,但频繁阻塞,吞吐上不去
- 典型值取 2–4 倍平均单次消费耗时对应的产量,例如每 100ms 生一个、每 200ms 消一个,缓冲 3–5 就较稳
close(ch) 必须由生产者调用,且只能调一次
消费者靠 for v := range ch 自动退出,前提是 channel 被关闭;但若多个生产者,谁关、何时关就成了问题。
立即学习“go语言免费学习笔记(深入)”;
常见错误:
- 多个生产者都调
close(ch)→ panic: close of closed channel - 主 goroutine 在生产者还没 finish 就 close → 消费者提前退出,漏数据
- 用
defer close(ch)在生产函数里,但生产函数已 return,close 没执行到
安全做法:只由**最后一个完成的生产者**显式 close,或用 sync.WaitGroup 等所有生产者结束再 close:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 5; j++ {
ch <- id*10 + j
}
}(i)
}
go func() {
wg.Wait()
close(ch) // 所有生产者结束才关
}()消费者怎么知道该停、不停错、不漏
range ch 是最简方式,但它依赖 channel 关闭;如果 channel 没关,它就永远等下去。而有些场景(如长时监听消息队列),你并不想关 channel,而是靠外部信号退出。
这时得换方案:
- 用
select+context.WithCancel:消费者监听ch和ctx.Done()两个通道 - 避免在消费者里直接
close(ch)—— 消费者无权决定生产是否结束 - 别用
ok := 判断是否关闭后还继续循环,容易漏掉最后一条(因为 ok 为 false 时 value 是零值)
正确写法示例:
func consumer(ctx context.Context, ch <-chan int) {
for {
select {
case v, ok := <-ch:
if !ok {
return // channel 关了
}
fmt.Println("处理", v)
case <-ctx.Done():
return // 上层要求退出
}
}
}WaitGroup 和 channel 关闭顺序不能颠倒
这是最容易被忽略的竞态点:主 goroutine 先 wg.Wait() 再 close(ch) 是安全的;但如果反过来,close(ch) 发生在 wg.Wait() 之前,就可能出现消费者刚读完最后一项、正准备 exit,而生产者还在往已关闭的 channel 发数据 —— panic。
更隐蔽的是:消费者用 range 时,close(ch) 后 range 会自动退出,但若消费者 goroutine 已退出,而 wg.Done() 还没调,wg.Wait() 就永远卡住。
所以务必确保:
- 每个消费者在
range结束后显式调wg.Done() - 或改用
select+context,并在退出前调wg.Done() -
close(ch)动作必须在所有生产者 goroutine 完全退出后触发
实际编码中,建议把 “等待生产者” 和 “关闭 channel” 封装成一个独立 goroutine,和其他逻辑隔离。


















