Go 的 chan 仅适合教学演示或单机协程通信,因其不持久、不跨进程、无 ACK、易阻塞泄漏,且缺乏幂等、重试、死信等生产级 MQ 功能。

Go 语言内置的 chan 可以快速实现“伪消息队列”,但它不是生产级解耦方案——它不持久、不跨进程、无法重试,只适合教学演示或单机内部协程通信。
用 chan 模拟队列:适合学习 goroutine 和 channel 基础
这是理解“生产者-消费者”模型最直接的方式,但必须清楚它的边界:
-
chan是内存中的管道,程序退出即丢失全部消息 - 没有 ACK 机制,发送方不知道接收方是否处理成功
- 无缓冲
chan会阻塞发送,缓冲区满也会阻塞,容易导致 goroutine 泄漏 - 无法做幂等、重试、死信、路由等真实 MQ 功能
示例仅用于验证逻辑流:
msgCh := make(chan string, 10)
go func() {
for msg := range msgCh {
fmt.Println("处理:", msg)
time.Sleep(100 * time.Millisecond) // 模拟耗时操作
}
}()
msgCh <- "订单创建"
msgCh <- "发短信通知"
close(msgCh)
为什么不能把 chan 当成 RabbitMQ 或 Kafka 的替代品
常见误用场景和后果:
立即学习“go语言免费学习笔记(深入)”;
- 在 HTTP handler 里直接往
chan发消息,然后返回响应 —— 若消费者 goroutine 崩溃或未启动,消息就永久丢失 - 用
chan替代服务间通信,结果两个微服务部署在不同机器上,chan根本不通 - 为“异步”而异步:把数据库写入扔进
go func(){...}(),没加错误捕获和重试,失败后无日志、无告警、无补偿
真正解耦 = 消息落地 + 显式确认 + 可追溯。这三件事 chan 一件都做不到。
amqp.Publishing 必须设 DeliveryMode: amqp.Persistent
RabbitMQ 默认发的是内存消息,Broker 重启就清空。很多初学者调了 ch.Publish() 却收不到消息,查日志也没报错,就是因为漏了这一行:
err := ch.Publish(
"", // exchange
"task_queue",
false, // mandatory
false, // immediate
amqp.Publishing{
ContentType: "text/plain",
Body: []byte("hello"),
DeliveryMode: amqp.Persistent, // ← 关键!否则等于没发
Mandatory: true, // ← 路由失败立刻报错,不静默丢弃
})
-
DeliveryMode: amqp.Transient表示仅存内存,性能略高但不可靠 -
Mandatory: true强制校验 routing key 是否匹配,避免消息进黑洞 - 二者缺一,都算“假异步”——表面跑通,实际不可靠
消费者必须第一行写 defer msg.Ack(false)
这是最容易被忽略、也最常引发积压的坑。RabbitMQ 的消费模型是“预取 + 手动确认”,不是“发完就算”。不显式 Ack,消息就一直卡在 unacked 状态:
- 如果业务逻辑中间 panic 或 return,
msg.Ack(true)就永远不会执行 - 消息持续堆积,channel 被流控阻塞,整个消费者停摆
- 正确做法:开头 defer 保证无论怎么退出都先释放消息(
false表示拒绝并重回队列),成功后再msg.Ack(true)
典型结构:
for msg := range msgs {
defer msg.Ack(false) // ← 第一行就放这里
if err := process(msg.Body); err != nil {
log.Printf("处理失败: %v", err)
continue
}
msg.Ack(true) // ← 成功才确认
}
真解耦不靠快,靠稳;不靠“发出去就行”,靠“发出去、收到、干完、说一声”。任何跳过确认环节的设计,本质上都是在赌系统不出问题。


















