应定义函数式 Notifier 接口,接收 context 和已渲染 Message,Channel 用常量;SMTP 需用中转服务或合理超时配置;短信须基于 MessageID 幂等;站内信未读数存 Redis,列表用游标分页。

怎么设计统一的消息通道接口
Go 里没有内置的“通知服务”抽象,硬写一堆 SendEmail()、SendSMS() 函数会导致调用方耦合严重,改一个渠道就要动业务逻辑。必须先定义清晰的接口,让渠道实现可插拔。
推荐用函数式接口,不强制依赖 struct 或 interface{}:
type Notifier interface {Notify(ctx context.Context, msg *Message) error }
其中 Message 至少包含 ID、UserID、Title、Content、Channel(如 "email"、"sms"、"inapp"),避免后续加字段时所有实现全量修改。
- 别把模板渲染逻辑塞进
Notify()—— 渲染应前置,传入已渲染好的纯文本 /HTML -
Channel字段建议用常量定义(const ChannelEmail = "email"),别用字符串字面量散落各处 - 接口方法必须接收
context.Context,否则超时控制、取消传播全失效
邮件发送为什么总卡住或报错 dial tcp: i/o timeout
这不是 Go 代码问题,而是 SMTP 配置和网络策略导致的高频失败点。本地开发用 smtp.gmail.com:587 很容易失败,因为 Gmail 默认禁用“不够安全的应用”。
立即学习 “go 语言免费学习笔记(深入)”;
- 生产环境别直连公有邮箱 SMTP,用 Mailgun / SendGrid / Amazon SES 等中转服务,它们提供稳定 API + 可观测性
- 若必须自建 SMTP,确保
net.DialTimeout设置合理(建议 ≤10s),并启用STARTTLS(不是 SSL/TLS) - Go 标准库
net/smtp不支持 OAuth2,Gmail/Outlook 账号需生成专用 App Password 替代账户密码 - 错误日志里看到
dial tcp: i/o timeout,优先检查防火墙、VPC 安全组是否放行目标端口,而不是重试逻辑
短信网关怎么避免重复投递和乱序
第三方短信服务(如阿里云、腾讯云)本身不保证投递顺序,且 HTTP 请求可能因网络重试造成重复。Go 层必须做幂等控制,不能指望下游。
- 每个消息生成唯一
MessageID(用uuid.NewString()),并透传给短信服务商的ExtData或Signature字段 - 在数据库加唯一索引:(
channel,ext_id),插入前先INSERT …… ON CONFLICT DO NOTHING(PostgreSQL)或INSERT IGNORE(MySQL) - 别用时间戳或用户 ID 做幂等键——同一用户发两条相似内容会冲突
- 异步发送时,用
time.AfterFunc或简单轮询查状态,别依赖短信回调——回调可能延迟数分钟甚至丢失
站内信怎么做到低延迟又不压垮数据库
站内信本质是高读低写场景,但“未读数实时更新”和“列表分页拉取”极易成为瓶颈。直接 SELECT * FROM notifications WHERE user_id = ? AND read = false 在百万级数据下必然慢。
- 把未读数存在 Redis 的
HSET user:<id> unread_count 12</id>,每次新消息 +1,标记已读 -1;DB 只存原始记录,不承担计数压力 - 消息列表用“游标分页”而非
OFFSET:按created_at DESC, id DESC排序,请求带cursor=2024-03-19T10:00:00Z_12345,SQL 改为WHERE created_at - 冷数据归档:6 个月前的已读消息迁出主表,避免
WHERE user_id = ?全表扫描 - 别在事务里发通知——通知失败不该回滚业务操作(比如下单成功但站内信没发出去,订单仍应成立)
多渠道推送真正难的不是发,是状态收敛、失败重试策略、以及用户关闭某渠道后的历史消息屏蔽逻辑。这些细节不提前约定清楚,上线后查漏补缺的成本远高于初期多花两小时建模。