make(chan t, n) 就是 go 中最轻量、天然 fifo 且并发安全的内存消息队列;它不是模拟,本身就是有界阻塞队列,缓冲大小 n 决定积压能力,而无缓冲 channel 仅为同步点,无暂存能力,不能用作队列。
make(chan t, n) 就是你要的答案——它不是“模拟”队列,它本身就是 go 里最轻量、天然 fifo、并发安全的内存消息队列。
为什么用带缓冲 channel 而不是无缓冲
无缓冲 chan T 是同步点,一发一收,不存消息,根本不能当队列用。你写 ch 会卡住,直到另一边 <code> 准备好;这适合通知信号,不适合暂存任务。
- 带缓冲
make(chan Task, 100)才有积压能力:发送不阻塞(只要没满),接收不阻塞(只要没空) - 缓冲大小
N是关键权衡点:设为1和无缓冲几乎没区别;设为10000可能掩盖消费慢的真实瓶颈,还吃内存 - 真实项目建议用结构体类型,比如
type Task struct { ID string; Payload []byte },别用string或[]byte直接传,避免序列化歧义
多生产者 + 安全关闭的正确姿势
多个 goroutine 往同一个 chan 发消息完全没问题,但关通道只能关一次,且必须等所有生产者都退出后再关——否则消费者 range ch 会提前结束,或运行时报 panic: close of closed channel。
- 别让每个生产者都调
close(ch) - 用
sync.WaitGroup计数生产者数量,在最后一个生产者完成时,由单独一个 goroutine 调close(ch) - 消费者必须用
for range ch,而不是单次:后者在通道关闭后不会自动退出,会读零值或卡死
防阻塞和超时控制必须手动加
默认行为是阻塞:ch 满了就卡死,<code> 空了也卡死。线上服务不能接受无限等待。
- 生产者非阻塞写入:
select {
case ch
- 消费者带超时读取:
select {
case msg :=
- 注意:
select里不能写两个case,Go 会随机选一个,破坏 FIFO 语义
它的边界在哪,别指望它做做不到的事
用 chan 做队列,就得接受它的设计事实:
- 不持久:进程重启,消息全丢
- 不跨进程:其他语言或进程无法访问这个
chan - 不保“完成序”:多个消费者并发读,处理耗时不一,
ID=1的消息可能比ID=2晚处理完 - 积压量可查但别混淆:
len(ch)是当前已缓存数,cap(ch)是固定容量,不是剩余空间 - 需要不丢消息?该上 Redis 的
LPush/BRPop或 RabbitMQ
封装成 type MemQueue struct { ch chan Task } 不是炫技,是为后续加监控、限流、统计埋点留接口——但前提是,你真只需要内存队列。











