前言
学习 https://zhuanlan.zhihu.com/p/665893174
✅ go-nsq 库的优点
- 官方维护、Go 原生风格:API 简洁符合 Go 习惯(HandlerFunc / Producer / Consumer),学习成本极低
- 充分利用 goroutine + channel:高并发消息处理,单机可支撑高吞吐
- 自动服务发现:通过 nsqlookupd 自动发现 nsqd 节点,支持动态扩缩容
- 完善连接管理:内置重连、心跳、背压控制(MaxInFlight)、消息 ACK / Requeue / 超时控制
- 配置丰富:支持 TLS、压缩、认证、异步发布(PublishAsync)、消费重试与退避
⚠️ go-nsq / NSQ 的局限与注意点
| 类别 | 说明 |
|---|---|
| 消息不保证有序 | NSQ 无 partition,同一 channel 内也不保序 |
| At-Least-Once 投递 | 可能重复投递,业务需自己做幂等 |
| 无事务 / 无回溯 | 不像 Kafka 可按 offset 回溯历史消息,无分布式事务 |
| 默认内存优先 | 消息主要在内存,重启可能丢(需配 --mem-queue-size=0 才强制落盘) |
| 消息体大小限制 | 默认单条 ≤1MB,超大会被拒绝 |
| 无原生消费者组协调 | 多实例消费靠 nsqlookupd 发现 + channel 名相同实现,无 Kafka 那种 group coordinator |
- 使用注意:Handler 里不要随意起 goroutine 且不控制并发,需正确设置 MaxInFlight、调用 Stop() + WaitStopped() 防消息丢失或 goroutine 泄漏
- NSQ 本身运维局限:无原生 K8s Operator,topic 创建需调 HTTP API;nsqlookupd 挂掉不影响已连消费者但影响新节点发现
✅ 适合 & 不适合场景
适合:
• Go 微服务异步解耦、任务队列、日志聚合、实时事件推送
• 中小规模、追求轻量部署和运维低成本
不适合:
- 严格顺序消费(库存扣减、金融交易)
- 消息需精确一次投递 / 事务消息
- 超大规模流式处理(日千亿级,建议 Kafka/Pulsar)
go-nsq 是 Go 生态里最顺手的轻量消息队列客户端,配合 NSQ 适合高吞吐、低延迟、最终一致性场景;如果需要严格有序、exactly-once、消息回溯,应选 Kafka / RocketMQ / Pulsar。
如果选择go-nsq, 运行代码前需要用docker跑起来一个镜像
bash
docker run -d --name nsqd -p 4150:4150 -p 4151:4151 nsqio/nsq /nsqd
demo
go
package main
import (
"fmt"
"log"
"sync"
"time"
"github.com/nsqio/go-nsq"
)
const (
// 用于测试的消息主题
testTopic = "my_test_topic"
// nsqd 服务端地址
nsqdAddr = "localhost:4150"
// 订阅 channelGroupA 的消费者 A1
consumerA1 = "consumer_a1"
// 订阅 channelGroupA 的消费者 A2
consumerA2 = "consumer_a2"
// 单独订阅 channelGroupB 的消费者 B
consumerB = "consumer_b"
// testTopic 下的 channelA
channelGroupA = "channel_a"
// testTopic 下的 channelB
channelGroupB = "channel_b"
)
// 建立 consumer 与 channel 之间的映射关系
// consumerA1、consumerA2 -> channelA
// consumerB -> channelB
var consumerToChannelGroup = map[string]string{
consumerA1: channelGroupA,
consumerA2: channelGroupA,
consumerB: channelGroupB,
}
func runNsq() {
msgCallback := func(consumerName string, msg []byte) error {
fmt.Printf("i am %s, receive msg: %s\n", consumerName, msg)
return nil
}
if err := runProducer(); err != nil {
log.Fatalf("producer: %v", err)
}
var wg sync.WaitGroup
for consumer := range consumerToChannelGroup {
wg.Add(1)
go func(consumer string) {
defer wg.Done()
if err := runConsumer(consumer, msgCallback); err != nil {
log.Printf("consumer %s: %v", consumer, err)
}
}(consumer)
}
wg.Wait()
}
// 运行生产者
func runProducer() error {
// 通过 addr 直连 nsqd 服务端
producer, err := nsq.NewProducer(nsqdAddr, nsq.NewConfig())
if err != nil {
return err
}
defer producer.Stop()
// 通过 producer.Publish 方法,往 testTopic 中发送三条消息
for i := 0; i < 3; i++ {
msg := fmt.Sprintf("hello xiaoxu %d", i)
if err := producer.Publish(testTopic, []byte(msg)); err != nil {
return err
}
}
return nil
}
// 用于处理消息的 processor,需要实现 go-nsq 中定义的 msgProcessor interface,核心是实现消息回调处理方法: func HandleMessage(msg *nsq.Message) error
type msgProcessor struct {
// 消费者名称
consumerName string
// 消息回调处理函数
callback func(consumerName string, msg []byte) error
}
func newMsgProcessor(consumerName string, callback func(consumerName string, msg []byte) error) *msgProcessor {
return &msgProcessor{
consumerName: consumerName,
callback: callback,
}
}
// 消息回调处理
func (m *msgProcessor) HandleMessage(msg *nsq.Message) error {
// 执行用户定义的业务处理逻辑
if err := m.callback(m.consumerName, msg.Body); err != nil {
return err
}
// 倘若业务处理成功,则调用 Finish 方法,发送消息的 ack
msg.Finish()
return nil
}
// 运行消费者
func runConsumer(consumerName string, callback func(consumerName string, msg []byte) error) error {
// 根据消费者名获取到对应的 channel
channel, ok := consumerToChannelGroup[consumerName]
if !ok {
return fmt.Errorf("bad name: %s", consumerName)
}
// 指定 topic 和 channel,创建 consumer 实例
consumer, err := nsq.NewConsumer(testTopic, channel, nsq.NewConfig())
if err != nil {
return err
}
defer consumer.Stop()
// 添加消息回调处理函数
consumer.AddHandler(newMsgProcessor(consumerName, callback))
// consumer 连接到 nsqd 服务端,开启消费流程
if err = consumer.ConnectToNSQD(nsqdAddr); err != nil {
return err
}
<-time.After(5 * time.Second)
return nil
}
func main() {
runNsq()
}
bash
andydennis@AndydeMacBook-Pro goDraft % go run main.go
2026/06/01 14:29:48 INF 1 (localhost:4150) connecting to nsqd
2026/06/01 14:29:48 INF 1 (localhost:4150) stopping
2026/06/01 14:29:48 INF 1 (localhost:4150) exiting router
2026/06/01 14:29:48 INF 1 (localhost:4150) beginning close
2026/06/01 14:29:48 INF 1 (localhost:4150) readLoop exiting
2026/06/01 14:29:48 INF 1 (localhost:4150) breaking out of writeLoop
2026/06/01 14:29:48 INF 1 (localhost:4150) writeLoop exiting
2026/06/01 14:29:48 INF 4 [my_test_topic/channel_b] (localhost:4150) connecting to nsqd
2026/06/01 14:29:48 INF 2 [my_test_topic/channel_a] (localhost:4150) connecting to nsqd
2026/06/01 14:29:48 INF 3 [my_test_topic/channel_a] (localhost:4150) connecting to nsqd
i am consumer_b, receive msg: hello xiaoxu 0
i am consumer_a1, receive msg: hello xiaoxu 0
i am consumer_a2, receive msg: hello xiaoxu 1
i am consumer_a2, receive msg: hello xiaoxu 2
i am consumer_b, receive msg: hello xiaoxu 1
i am consumer_b, receive msg: hello xiaoxu 2
2026/06/01 14:29:48 INF 1 (localhost:4150) finished draining, cleanup exiting
2026/06/01 14:29:48 INF 1 (localhost:4150) clean close complete
2026/06/01 14:29:53 INF 2 [my_test_topic/channel_a] stopping...
2026/06/01 14:29:53 INF 3 [my_test_topic/channel_a] stopping...
2026/06/01 14:29:53 INF 4 [my_test_topic/channel_b] stopping...