通知发送逻辑与Gin路由解耦的核心是定义统一Notifier接口,由各渠道实现Send方法;路由仅解析请求并调用notifier.Send(),不关心具体实现;模板渲染交由独立TemplateEngine统一管理,Webhook需校验、签名、限流及异步重试,日志由notifier内部结构化记录。

通知发送逻辑怎么和 Gin 路由解耦?
硬把 sendEmail、sendSms 塞进控制器里,后面加钉钉、飞书、企业微信就只能复制粘贴改函数名。真正该做的是定义统一的 Notifier 接口,让每种渠道实现自己的 Send(context.Context, *Notification) error 方法。Gin 路由只负责解析请求、校验参数、调用 notifier.Send(),不关心具体发给谁、怎么发。
-
Notification 结构体必须包含 TemplateID、Params(map[string]interface{})、Targets(手机号/邮箱列表),避免每个渠道自己解析 JSON
- 初始化时用 map 注册不同 notifier:
notifiers["email"] = &EmailNotifier{...},路由里通过 notifiers[req.Channel] 获取实例
- 不要让 Gin 中间件去“拦截并重试失败通知”,重试逻辑属于 notifier 实现层,比如
SmsNotifier 内部用 backoff.Retry 包封装三次重试
模板渲染该放在哪一层?
有人在前端拼接好完整消息体再 POST 过来,结果模板变更就得前后端一起改;也有人在每个 notifier 里重复写 text/template 渲染逻辑,导致邮件模板和短信模板语法不一致。正确做法是单独抽一个 TemplateEngine,统一管理所有渠道的模板文件。
- 模板路径按渠道+类型组织:
templates/email/welcome.html、templates/sms/verify.txt
- 加载时用
template.ParseFS(Go 1.16+)或 template.ParseGlob,避免运行时读文件出错没提示
- 渲染前校验
Params 是否包含模板所需字段,缺失时直接返回 ErrMissingTemplateParam,而不是渲染出 “{{.UserName}}” 这种裸变量
怎么安全地支持用户自定义 Webhook?
开放 Webhook 是刚需,但直接 http.Post 第三方地址容易引发服务雪崩:对方超时、返回 503、甚至故意返回超大响应体拖垮内存。必须加管控层。
- Webhook URL 必须经
url.Parse 校验协议和 host,禁止 file://、javascript: 等危险 scheme
- 请求头固定加
X-Notification-ID 和 X-Signature(HMAC-SHA256 + 密钥),方便接收方验签
- 使用带 timeout 的
http.Client:&http.Client{Timeout: 5 * time.Second},且限制响应体大小(http.MaxBytesReader)
- 失败时不立即重试,而是写入 Redis 队列,由独立 worker 按指数退避重发
Gin 中间件里能直接写通知日志吗?
不能。中间件职责是鉴权、限流、记录请求耗时,不是业务日志。通知是否成功、送达状态、渠道响应码这些,必须由 notifier 实现内部记录,且日志格式要统一(例如都带 notification_id、channel、status 字段)。
- 日志写入推荐用
log/slog(Go 1.21+),避免 log.Printf 无法结构化的问题
- 成功日志级别设为
INFO,失败且不可重试(如模板不存在)设为 ERROR,临时失败(如网络超时)设为 WARN
- 别在日志里打印
Params 全量内容,敏感字段如手机号、邮箱需脱敏处理(redactPhone("138****1234"))
Notification 结构体必须包含 TemplateID、Params(map[string]interface{})、Targets(手机号/邮箱列表),避免每个渠道自己解析 JSON notifiers["email"] = &EmailNotifier{...},路由里通过 notifiers[req.Channel] 获取实例 SmsNotifier 内部用 backoff.Retry 包封装三次重试 text/template 渲染逻辑,导致邮件模板和短信模板语法不一致。正确做法是单独抽一个 TemplateEngine,统一管理所有渠道的模板文件。
- 模板路径按渠道+类型组织:
templates/email/welcome.html、templates/sms/verify.txt - 加载时用
template.ParseFS(Go 1.16+)或template.ParseGlob,避免运行时读文件出错没提示 - 渲染前校验
Params是否包含模板所需字段,缺失时直接返回ErrMissingTemplateParam,而不是渲染出 “{{.UserName}}” 这种裸变量
怎么安全地支持用户自定义 Webhook?
开放 Webhook 是刚需,但直接 http.Post 第三方地址容易引发服务雪崩:对方超时、返回 503、甚至故意返回超大响应体拖垮内存。必须加管控层。
- Webhook URL 必须经
url.Parse 校验协议和 host,禁止 file://、javascript: 等危险 scheme
- 请求头固定加
X-Notification-ID 和 X-Signature(HMAC-SHA256 + 密钥),方便接收方验签
- 使用带 timeout 的
http.Client:&http.Client{Timeout: 5 * time.Second},且限制响应体大小(http.MaxBytesReader)
- 失败时不立即重试,而是写入 Redis 队列,由独立 worker 按指数退避重发
Gin 中间件里能直接写通知日志吗?
不能。中间件职责是鉴权、限流、记录请求耗时,不是业务日志。通知是否成功、送达状态、渠道响应码这些,必须由 notifier 实现内部记录,且日志格式要统一(例如都带 notification_id、channel、status 字段)。
- 日志写入推荐用
log/slog(Go 1.21+),避免 log.Printf 无法结构化的问题
- 成功日志级别设为
INFO,失败且不可重试(如模板不存在)设为 ERROR,临时失败(如网络超时)设为 WARN
- 别在日志里打印
Params 全量内容,敏感字段如手机号、邮箱需脱敏处理(redactPhone("138****1234"))
url.Parse 校验协议和 host,禁止 file://、javascript: 等危险 scheme X-Notification-ID 和 X-Signature(HMAC-SHA256 + 密钥),方便接收方验签 http.Client:&http.Client{Timeout: 5 * time.Second},且限制响应体大小(http.MaxBytesReader) notification_id、channel、status 字段)。
- 日志写入推荐用
log/slog(Go 1.21+),避免log.Printf无法结构化的问题 - 成功日志级别设为
INFO,失败且不可重试(如模板不存在)设为ERROR,临时失败(如网络超时)设为WARN - 别在日志里打印
Params全量内容,敏感字段如手机号、邮箱需脱敏处理(redactPhone("138****1234"))
通知系统真正的复杂点不在“发出去”,而在“发没发成”和“怎么证明发成了”。渠道响应不可靠、第三方回调延迟、模板变更影响范围难评估——这些才是上线后天天要盯的日志和告警项。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。

















