正确做法是生产者完成所有发送后关闭channel,消费者用for range安全接收;多生产者时改用WaitGroup+done channel;任务队列需缓冲和超时机制防卡死。

用 chan 实现基础生产者消费者,别直接关 channel
Go 里最自然的实现就是靠 chan,但新手常在关闭 channel 上栽跟头。比如生产者 goroutine 写完就 close(ch),消费者却还在读 —— 立刻 panic:panic: send on closed channel 或 invalid memory address(取决于读写时序)。正确做法是只由生产者决定“不发了”,消费者用 for range ch 安全接收,它会在 channel 关闭且缓冲区清空后自动退出。
关键点:
- 生产者负责
close(ch),且必须确保所有发送完成后再关(不能边发边关) - 消费者统一用
for v := range ch,不用for { select { ... }}加手动判断 - 如果多个生产者,别随便 close —— 改用
sync.WaitGroup+donechannel 协调
带缓冲和超时的 task queue,select 配 default 很关键
真实任务队列不能卡死,比如消费者处理慢、生产者突发大量任务时,得有背压控制。用带缓冲的 chan(如 make(chan Task, 100))能缓解,但缓冲满后 ch 会阻塞。这时候要用 <code>select 配 default 做非阻塞写入,再配合超时 fallback:
select {
case q.tasks <- task:
// 成功入队
default:
// 缓冲满,可记录日志、丢弃或走降级逻辑
log.Warn("task queue full, dropping task")
}
注意:default 分支会让写入变成“尽力而为”,不是错误,所以别把它当异常处理;真要强一致性,就得用无缓冲 channel + 调用方自己 handle block。
立即学习“go语言免费学习笔记(深入)”;
消费者并发数固定但任务处理可能 panic,recover 必须包在 goroutine 里
启动 5 个消费者 goroutine 处理 chan,其中一个 panic 会导致整个程序崩溃 —— 因为 panic 不会跨 goroutine 传播,但没 recover 就会终止该 goroutine,表面看只是少一个 worker,实际可能因任务积压引发雪崩。
正确写法是每个消费者 goroutine 内部做 recover:
go func() {
defer func() {
if r := recover(); r != nil {
log.Error("consumer panic", "err", r)
}
}()
for task := range ch {
task.Process()
}
}()
常见错法:把 recover 放在启动 goroutine 的外层函数里,完全无效;也别依赖 log.Panic 这类会主动 exit 的日志 —— 它照样停进程。
需要任务结果回传?别用双向 channel 塞一堆 chan Result
有些场景要求“发任务 → 拿结果”,比如 RPC 调用或异步校验。错误做法是让每个 Task 带一个 replyCh chan 字段,然后生产者新建 channel 塞进去 —— 这会产生大量短命 goroutine 和 channel,GC 压力大,还容易漏关。
更轻量的做法是用结构体字段存 result *Result + err error,配合 sync.Once 或 CAS 更新;如果真要 channel 回传,统一用一个结果 channel(chan Result),由消费者写入,调用方按 request ID 匹配。重点是:结果 channel 生命周期应由调用方管理,别让生产者/消费者负责创建和关闭。
复杂点在于消费者数量、任务生命周期、错误重试策略这三者的耦合 —— 很多 bug 不出在 channel 语法,而出在没想清楚谁该负责超时、谁该负责重试、谁该决定任务是否算“失败”。


















