做后端开发的基本都躲不开生产者消费者模式:面试题里年年出现的 wait/notify,工作里天天接触的 Kafka、RabbitMQ,本质上都在解决同一件事——把生产消息和消费消息两个动作拆开,让它们不用互相等。这次我用 Go 从零手写了一个内存版消息队列系统,目标很直接:用 goroutine 加 channel 把生产者消费者模式做扎实,同时把高并发环境下队列关闭、优雅退出、背压、panic 隔离这些容易翻车的点一个一个踩平。
这个项目核心代码只有几百行,不需要依赖任何第三方库,跑起来之后能清晰看到生产端和消费端的协作关系,也能自己动手改参数观察队列行为。适合正在学 Go 并发、面试前想补一补高并发知识点、或者想在项目里引入轻量级异步处理但不想上重型中间件的人。如果你最后想玩转 Kafka 这类分布式消息队列,先把这个单机版本吃透,底层思路都是相通的。
1. 项目从哪来:先明白消息队列解决的到底是什么问题
1.1 需求拆解:日志异步落盘这个场景
我最初做这个项目是因为手头有个服务需要写操作日志,量不大但很频繁。如果每次请求都同步写磁盘,接口延迟会被 IO 拖住;如果把日志全部攒在内存里又怕堆积太多把内存撑爆。这种场景就是典型的生产者消费者问题:请求线程是生产者,日志写入线程是消费者,中间需要一段缓冲区,让请求不用等磁盘,同时缓冲区有上限,防止内存被打满。
围绕这个场景,我给项目定了几个需求。第一,支持多个生产者并发往队列里丢消息;第二,支持多个消费者从队列里取消息处理,处理逻辑需要可替换;第三,队列满的时候不能让生产者无脑一直等,要支持超时或者直接返回错误;第四,程序关闭时要优雅退出,先把队列里的存量消息处理完,再释放资源;第五,要有消息数量统计,方便压测时看吞吐量。
这些需求对应到工程上就是:并发安全、缓冲容量控制、超时控制、生命周期管理、原子计数。等我把这些点全部用 Go 实现一遍之后,再看 Kafka 官方文档里说的“partition、buffer、consumer group”,会发现很多概念其实都能对应上,只是分布式场景下多了一套网络和副本机制。
1.2 为什么拿 Go 写而不是 Java 或 Python
单说结果的话,Java 的 BlockingQueue、Python 的 queue.Queue 其实都能实现类似功能,但我最终选了 Go,原因有两条。
第一,Go 的 goroutine 非常轻量,起十万个也不至于把内存吃穿。消息队列天生需要大量并发消费者,如果用 Java 线程做,一个消费者一个线程,量大了之后线程上下文切换成本很高;用 Go 的话,起几十个 goroutine 做 consumer worker 池,开销极低,代码写起来也直接,不用纠结线程池参数。
第二,channel 是 Go 语言内置的并发原语,它本身就是一个线程安全的 FIFO 队列。做消息队列的核心就是传递消息,而 channel 恰好把这个语义做进了语法层面。生产者往 channel 里发,消费者从 channel 里收,天然带同步和阻塞语义,不需要再引入任何中间件。对比一下,Java 里需要从 BlockingQueue 接口里选实现类,Python 里还要注意 GIL 对性能的影响,Go 写起来是最省心的。
| 维度 | Go chan | Java BlockingQueue | Python queue.Queue |
|---|---|---|---|
| 并发模型 | goroutine + channel | 线程 + 锁 | 线程 + 锁 |
| 代码简洁度 | 高 | 中,需要选实现类 | 中 |
| 性能上限 | 高,调度成本低 | 中高,线程切换有成本 | 中,受GIL影响 |
| 内置超时控制 | select + context 直接支持 | 需要 poll 或加锁实现 | get(timeout) 有限支持 |
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 核心实现:用 channel 搭一个最小可跑的队列系统
2.1 第一个设计决策:用无缓冲还是带缓冲 channel
channel 分两种:无缓冲和有缓冲。无缓冲 channel 的特点是发送方必须等接收方取走数据才返回,否则阻塞。这其实就是一个同步点,生产者和消费者必须同时在线才能完成一次交接。有缓冲 channel 则允许生产者在缓冲区未满时先放进去,不用等消费者,真正实现了生产消费的异步解耦。
消息队列当然要选有缓冲 channel,因为异步的本质就是允许消息在中间暂存。缓冲容量就是队列长度,决定了系统能容忍多大程度的“产消速度差”。我刚开始实现时用了一个固定容量的 channel,缓冲设为 1024,这个值不是拍脑袋定的,而是根据日志场景估算出来的:高峰期每秒大概产生 5000 条日志,消费者批量落盘可以做到每秒 10000 次,理论上缓冲区不会堆积,但为了保证 IO 抖动时不丢请求,留出 1024 的缓冲足够覆盖几秒钟的消费延迟。
真正动手的时候要注意一
