不用现成MQ而自封装,是因为内部服务通信无需Kafka/RabbitMQ的重量级保障,用channel加控制逻辑更轻量;封装重点是解决裸chan的卡死、panic等问题,而非造轮子。

为什么不用现成的 MQ,而要自己封装?
因为很多内部服务间通信并不需要 Kafka 或 RabbitMQ 那种重量级保障:没有跨机房部署、不追求百万级吞吐、也不需要消息持久化到磁盘。这时候用 channel + 少量控制逻辑就能搞定,还能避免引入额外依赖和运维成本。
但直接裸用 chan 容易出问题——比如消费者挂了没人收,生产者就卡死;或者没做缓冲,突发流量直接 panic。所以封装重点不是“造轮子”,而是补上这些断点。
用 sync.Map 管理多个队列实例是否合理?
不合理。多数轻量场景下,一个服务只用 1–3 个逻辑队列(如 "email"、"notify"),用 sync.Map 反而增加锁开销和 GC 压力。更简单可靠的做法是:
- 每个队列对应一个独立结构体实例,由调用方显式初始化
- 如果需要多队列,用普通 map[string]*Queue +
sync.RWMutex保护 map 本身(只读多写少) - 避免在
Put/Get路径上做 map 查找——这会成为性能热点
示例初始化:
q := NewQueue(100) // 缓冲区大小 100
立即学习“go语言免费学习笔记(深入)”;
Put 方法要不要阻塞?
取决于下游消费能力是否可预期。默认建议非阻塞 + 返回错误:
- 用
select+default判断缓冲区满,立刻返回ErrQueueFull - 不阻塞能防止上游协程积压,也便于做限流或降级(比如日志队列满了就丢弃)
- 真需要阻塞语义,暴露一个
PutBlock方法,内部用select等待chan可写,但必须带超时
关键点:不要让 Put 成为潜在 hang 点。生产环境里,一个卡住的 Put 往往会拖垮整个 HTTP handler。
消费者怎么安全退出?
最常被忽略的是:关闭 channel 后,正在 range 的 goroutine 会立即退出,但可能漏掉最后一批已入队但未处理的消息。正确做法是:
- 用
close()关闭 channel 前,先调用drain()把剩余消息取完 - 提供
Shutdown()方法,内部做两件事:标记停止接收新消息 + 等待当前所有消息处理完毕 - 用
sync.WaitGroup记录活跃处理中的消息数,而不是依赖 channel 关闭信号
否则你会遇到“明明调了 Close,但还有几条消息没打印出来”的情况——那是因为 range 已退出,剩下的还在 channel 缓冲区里躺着。
真正难的不是实现 put/get,而是定义清楚“一条消息算不算被成功处理”。这个边界一旦模糊,重启后丢消息或重复投递就很难排查。


















