用 rdb.subscribe(ctx, "topic") 获取 *redis.pubsub 实例,需 defer pubsub.close() 防泄漏;支持多频道订阅但需靠 msg.channel 区分;receivemessage() 阻塞读,须分类处理 context.canceled、redis.nil 和其他错误;publish 返回在线订阅数非成功标志,且 pub/sub 无消息持久化。

怎么用 go-redis 启动一个基础 Pub/Sub 连接
直接用 rdb.Subscribe(ctx, "topic") 就能拿到一个 *redis.PubSub 实例,它不是普通 client,不能执行 GET 或 SET;它是专为频道通信设计的独立连接。别把它和主 client 混用,也别拿它去发消息——发布得用 rdb.Publish()。
- 必须调用
defer pubsub.Close(),否则连接泄漏,Redis 侧会堆积client list中的 idle 连接 -
Subscribe是阻塞式初始化,如果频道名拼错(比如多空格或大小写不一致),不会报错,但后续ReceiveMessage()永远收不到——这是最常被忽略的“静默失败” - 一个
PubSub实例可同时订阅多个频道:rdb.Subscribe(ctx, "order.created", "user.updated"),但所有消息都走同一个ReceiveMessage()流,需靠msg.Channel区分来源
为什么 ReceiveMessage() 会卡住或 panic
它本质是阻塞读,底层依赖 Redis 的 SUBSCRIBE 响应流。一旦网络抖动、Redis 重启或 context 被 cancel,ReceiveMessage() 可能返回 redis.Nil、context.Canceled 或真实错误,但初学者常只检查 err != nil 就 break,导致 goroutine 提前退出、消息丢失。
- 正确做法:对每类错误分别处理——
errors.Is(err, context.Canceled)表示主动退出;errors.Is(err, redis.Nil)可忽略(心跳超时);其他 err 才该打日志并重试 - 别在 for 循环里直接
time.Sleep(100 * time.Millisecond)等待,这会放大延迟;应该用ctx控制整体生命周期,例如ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) - 接收消息后,务必检查
msg.Payload长度是否为 0,Redis 允许发空字符串,但业务逻辑通常不期望它
如何避免发布端阻塞或订阅端收不到消息
发布者调用 rdb.Publish(ctx, "topic", data) 后,返回值 ret.Val() 是**当前在线且已成功订阅该频道的客户端数量**,不是“发送成功与否”的布尔值。如果返回 0,说明此刻没有活跃订阅者——但这不等于失败,只是无人在线收。
- 发布时不要依赖
ret.Err()判断业务逻辑是否生效;它只反映网络/权限问题(如 Redis 密码错误、ACL 拒绝PUBLISH权限) - 订阅端启动必须早于发布端,或者用 Redis Stream 替代纯 Pub/Sub——因为原生 Pub/Sub **不保存历史消息**,断连期间发布的消息彻底丢失
- 若需“至少一次”语义,得自己加 ACK 机制:订阅者处理完消息后,向另一个频道发确认,发布者监听该频道做幂等标记
要不要用 context.WithValue() 传身份信息
可以传,但仅限调试日志,别用于逻辑分支。比如在 publish() 和 subscribe() 中用 context.WithValue(ctx, "role", "payment_service"),然后日志里输出 ctx.Value("role"),方便排查谁在哪个频道发了什么。
立即学习“go语言免费学习笔记(深入)”;
- 切勿把业务参数(如 user_id、order_id)塞进 context,它不是数据传输通道;这些该放在
message体里,用结构体序列化后发 -
context.WithValue是浅拷贝,高并发下无性能问题,但滥用会导致 context 膨胀、GC 压力上升 - 真正要隔离环境(如测试/生产频道),应该用不同频道名前缀:
"prod:order.shipped"而不是靠 context 分流
Pub/Sub 看似简单,但“没收到消息”这个问题,八成出在订阅启动时机、频道名一致性、或误把 PubSub 当通用 client 用——先盯死这三处,比加日志更有效。










