不能直接用interface{}定义通知通道,因其导致调用方需手动类型断言、重复校验,新增通道需大量修改;应以行为契约(如Send方法)统一入口,用NotificationRequest含ChannelType和json.RawMessage保留格式自由度,并通过错误类型、配置抽象、限流隔离及显式降级策略实现可维护性。

为什么不能直接用 interface{} 定义通知通道
第三方通知通道(比如短信、邮件、微信模板消息)表面看都是“发一条消息”,但实际参数结构、错误码、重试逻辑、签名方式差异极大。用 interface{} 或泛型空接口承接,会导致调用方必须手动类型断言、重复写校验逻辑,一加新通道就改一堆地方。
真正要统筹的不是“怎么发”,而是“谁来发、发给谁、发什么、失败怎么兜底”。所以规范起点应该是行为契约,不是数据结构。
- 每个通道必须实现
Send(ctx context.Context, req NotificationRequest) error方法,统一入口 -
NotificationRequest不定义具体字段,只保留ChannelType(如"sms"、"wechat_work")和RawPayload(json.RawMessage),把格式自由度留给具体实现 - 错误必须返回带上下文的自定义错误,如
ErrRateLimited、ErrInvalidSignature,不能只抛fmt.Errorf
如何让不同通道共享认证与限流配置
微信公众号需要 access_token,阿里云短信要 AccessKeyID/Secret,而企业微信机器人只用一个 webhook_url。硬编码配置或全局变量会让测试和多环境切换崩溃。
推荐做法是把配置抽象成可注入的依赖项,而不是通道实现的一部分:
立即学习“go语言免费学习笔记(深入)”;
- 定义
AuthConfig接口,含GetToken() (string, error)和Validate() error两个方法 - 各通道初始化时接收该接口实例,比如
sms.NewClient(sms.WithAuth(aliyunAuth)) - 限流统一用
golang.org/x/time/rate.Limiter,但每个通道实例持有一个独立限流器,避免微信模板消息和短信互相卡死
注意:不要在 Send 方法里做 token 刷新——它可能被并发调用,应由 AuthConfig 实现内部加锁或使用 sync.Once 缓存。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
怎样处理通道不可用时的降级与重试
不是所有通知都值得重试三次再进队列。短信失败可能因号码错,重试无意义;企业微信机器人超时却大概率是网络抖动,重试更合理。
关键是在模块层面暴露策略选择权,而非写死逻辑:
- 每个通道实现需返回明确的错误类型,例如
IsTransient(err) bool函数,用于判断是否值得重试 - 提供
WithRetryPolicy(retry.Policy)选项,支持按错误类型、最大次数、退避间隔配置,而不是所有通道共用一套retry.DefaultBackoff - 降级链路必须显式声明,比如 “短信失败 → 退到站内信 → 站内信也失败 → 记日志告警”,不能靠 try-catch 捕获 panic 后模糊处理
常见坑:把重试逻辑塞进 Send 方法内部。这会让单元测试无法控制重试行为,也违反单一职责——通道只负责“这一次发”,不负责“发几次”。
为什么 NotificationRequest 的 RawPayload 必须是 json.RawMessage
微信模板消息要求 {"touser":"xxx","template_id":"xxx",...},而 SendCloud 邮件 API 要 {"to":["a@b.com"],"subject":"xxx",...}。如果定义成 map[string]interface{},序列化时会丢失字段顺序、float64 变成 int(如 123 变 123.0),某些通道会校验 JSON 结构一致性。
json.RawMessage 是最轻量且零拷贝的选择:
- 调用方用
json.Marshal生成原始字节,直接传入,不经过 Go struct 解析再序列化 - 通道实现可直接透传给 HTTP body,或做局部字段替换(如填充时间戳)后再发
- 调试时打印
string(req.RawPayload)就是真实发出的内容,没有中间转换损耗
别为了“类型安全”强加一层中间 struct——通知通道的协议边界就在 JSON 字符串,越靠近协议层,越少意外。

















