不能直接 close(channel) 后继续 send,否则触发 panic: send on closed channel;生产者需在完成任务后仅 close 一次,消费者须用 for v, ok := range ch 或 for { select { case v, ok := <-ch: if !ok { break } }} 安全读取。

Go 用 channel + goroutine 实现生产者消费者模型,核心是避免手动加锁、不依赖第三方库,但必须注意缓冲区容量和关闭时机,否则容易死锁或 panic。
为什么不能直接 close(channel) 后继续 send
向已关闭的 chan 发送数据会触发 panic:panic: send on closed channel。生产者退出前若未协调好关闭顺序,消费者还在读,就极易出错。
- 生产者完成任务后应调用
close(ch),仅此一次 - 消费者必须用
for v, ok := 循环读取,靠 <code>ok判断 channel 是否关闭 - 不要在多个生产者中随意 close —— 只有最后一个完成的生产者才该关
带缓冲的 channel 和无缓冲 channel 性能差异明显
无缓冲 chan int 要求生产者和消费者严格同步(send 和 receive 必须同时就绪),适合强顺序控制;带缓冲如 make(chan int, 100) 允许短暂解耦,吞吐更高,但缓冲区过大会掩盖背压问题。
- 缓冲大小建议设为预期峰值并发量 × 单次批处理量,例如日志采集场景常用
make(chan *LogEntry, 1024) - 若消费者处理慢、缓冲满,生产者 goroutine 会阻塞在
ch ,这是天然背压机制,别急着加超时或丢弃逻辑 - 用
len(ch)查当前队列长度(非缓冲区容量),可用于监控积压情况
如何安全支持多个生产者 + 多个消费者
多生产者共用一个 channel 没问题,但关闭需额外同步;多个消费者可并行读同一 channel,无需额外锁。
立即学习“go语言免费学习笔记(深入)”;
- 用
sync.WaitGroup管理所有生产者完成信号,由主 goroutine 统一 close - 消费者数量不建议动态伸缩 —— 启动时固定启动 N 个
go consumer(ch)即可 - 若需优雅退出(如收到
os.Interrupt),应通过额外的done chan struct{}通知所有 goroutine,而非直接 close 工作 channel
package main
<p>import (
"fmt"
"sync"
"time"
)</p><p>func main() {
ch := make(chan int, 5)
var wg sync.WaitGroup</p><pre class='brush:php;toolbar:false;'>// 启动 2 个生产者
for i := 0; i < 2; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
for j := 0; j < 3; j++ {
ch <- id*10 + j
time.Sleep(100 * time.Millisecond)
}
}(i)
}
// 启动 3 个消费者
for i := 0; i < 3; i++ {
go func(id int) {
for v := range ch {
fmt.Printf("consumer %d got %d\n", id, v)
time.Sleep(200 * time.Millisecond)
}
}(i)
}
// 等待所有生产者结束,再关闭 channel
go func() {
wg.Wait()
close(ch)
}()
// 主 goroutine 不退出,等消费者自然结束
time.Sleep(3 * time.Second)}
真正难的不是写通这个模型,而是判断什么时候该用带缓冲 channel、什么时候该加超时、以及如何把「关闭 channel」这件事从业务逻辑里剥离开 —— 这些细节没处理好,上线后就是偶发 panic 或 goroutine 泄漏。










