使用 channel 可实现最简生产者消费者模型:生产者协程写入带缓冲的 channel 并关闭,消费者用 range 读取直至关闭;缓冲区大小影响阻塞行为,close 是避免消费者永久阻塞的关键。

用 channel 实现最简生产者消费者模型
Go 里没有内置的生产者消费者框架,但 channel 天然适配这个模式:生产者往 channel 写,消费者从 channel 读,阻塞/同步逻辑由 runtime 自动处理。
关键不是“怎么模拟”,而是“怎么避免常见死锁和 goroutine 泄漏”。下面直接给可运行的最小闭环:
func main() {
ch := make(chan int, 2) // 缓冲区大小很重要,后面会说
<pre class="brush:php;toolbar:false;">// 生产者
go func() {
for i := 0; i < 5; i++ {
ch <- i
fmt.Printf("produced: %d\n", i)
}
close(ch) // 必须关闭,否则消费者可能永远阻塞
}()
// 消费者
for val := range ch { // range 自动检测 closed 状态
fmt.Printf("consumed: %d\n", val)
}}
-
make(chan int, 2)建议设缓冲区,否则单个 goroutine 启动后可能卡在第一次(无消费者就绪) - 必须调用
close(ch),否则range ch永不退出;但只能由生产者关,且只能关一次 - 不要在多个 goroutine 里并发调用
close(),会 panic:close of closed channel
多个消费者怎么安全共用一个 channel
多个 goroutine 同时 range 或 是安全的,runtime 会自动负载分发——这是 Go channel 的核心设计保证。
立即学习“go语言免费学习笔记(深入)”;
但要注意:不能一边 range,一边还有其他 goroutine 在 close(),容易触发 panic: send on closed channel(如果生产者还没关完就关了)或消费者读到零值。
- 推荐结构:生产者负责
close(),所有消费者只读,用range或for { select { case v := - 如果消费者需要主动退出(比如超时),别用
range,改用select+donechannel 配合break标签跳出循环 - 缓冲区大小影响吞吐:太小 → 生产者频繁阻塞;太大 → 内存占用高、数据延迟大;一般按单次批量大小 × 并发消费者数预估
goroutine 泄漏的典型场景和修复
最常见的泄漏是:生产者提前退出但没关 channel,或消费者 panic 后没清理,导致剩余 goroutine 卡在 上永久等待。
- 永远不要在生产者中用
return跳过close(),建议用defer close(ch)(但注意:defer 在 goroutine 结束时才执行,要确保它真能执行到) - 消费者如果做 I/O 或计算可能 panic,需包
recover,否则 goroutine 消失但 channel 无人读,后续生产者会卡住 - 测试时加
time.Sleep()看是否还有 goroutine 活着?更可靠的是用runtime.NumGoroutine()对比启动前后数值
要不要用 sync.WaitGroup 控制生命周期
要用,但仅用于主 goroutine 等待所有生产者/消费者结束,**不能代替 channel 关闭逻辑**。
-
WaitGroup记录的是“谁启动了”,close()表达的是“数据发完了”——两者语义不同,混用会出问题 - 典型错误:先
wg.Wait()再close(ch)→ 消费者已退出,close()失去意义;或者close()太早,消费者还没启动完就开始读,读到零值 - 正确顺序:生产者完成写入 →
close(ch)→wg.Done();主 goroutinewg.Wait()等生产者结束即可,消费者靠range自行退出
实际写业务时,最易忽略的是缓冲区容量与消费者处理速度的匹配。哪怕 channel 关得再干净,如果消费者每秒只能处理 10 条,而生产者每秒推 100 条且缓冲区只有 5,很快就会阻塞生产者——这时该扩缓冲区,还是限速,还是加消费者,得看具体 SLA 和内存约束。


















