Go实现Webhook通知的核心是可靠发送:需指数退避重试(1s→2s→4s→8s,最多5次),仅对5xx和连接错误重试,跳过400/410/422等永久性错误。

直接用 http.Post 或 http.Do 发 Webhook,不加控制地 go send(),90% 的线上告警丢失都源于此——不是发不出去,而是发了没重试、发了没验错、发了阻塞主线程或丢进 goroutine 后彻底失控。
为什么不能在 handler 里直接 go send()
看似异步,实则埋雷:请求体 r.Body 在 handler 返回后被自动关闭,goroutine 里再读就是 io.EOF;panic 不会冒泡到 HTTP 层,悄无声息丢任务;进程重启时所有待执行 goroutine 全灭。真正该做的,是在 handler 中只做三件事:校验签名、io.ReadAll(r.Body) 拿到原始字节、写入持久化队列(如 Redis Stream 或数据库),然后立刻返回 http.StatusOK。
- 别传
*http.Request或io.ReadCloser进 goroutine - 业务逻辑必须基于已读取的
[]byte或序列化后的结构体启动 - 如果用 channel 中转,channel 必须带缓冲(如
make(chan []byte, 1000)),否则高并发下直接阻塞 handler
重试只对特定错误生效,不是所有失败都该重试
400 Bad Request、410 Gone、422 Unprocessable Entity 是永久性错误,重试等于刷垃圾请求。只有连接类错误(net.OpError、url.Error)和 5xx 状态码才值得重试,且必须指数退避:1s → 2s → 4s → 8s,最多 5 次。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
- 用
http.Client时务必设Timeout: 10 * time.Second,避免卡死连接池 - 每次
http.Do后必须defer resp.Body.Close(),哪怕只读resp.StatusCode - 判断是否重试前,先检查
err != nil,再检查resp != nil && resp.StatusCode >= 500
钉钉/企微等加签渠道,403 不是 token 错,是 sign 或 timestamp 错
开启加签后,URL 必须带 timestamp 和 sign 参数,缺一不可。timestamp 要毫秒级整数(time.Now().UnixMilli()),sign 是对 tsStr + " " + secret 做 HMAC-SHA256 再 base64 编码——secret 必须是创建机器人时页面显示的完整字符串(以 SEC 开头),截短或漏空格都会 403。
立即学习“go语言免费学习笔记(深入)”;
- Markdown 消息结构必须严格嵌套:
{"msgtype":"markdown","markdown":{"text":"# hello"}},顶层写"text"直接静默失败 - JSON 字符串内换行必须用
,不是\n或 HTML 标签 - 高频推送必须复用全局
*http.Client,禁止每次新建
最易被忽略的一点:重试逻辑必须和发送逻辑解耦,不能写成“发一次,失败就重试三次”。真正的可靠性来自“发送失败 → 写入延迟队列 → 由独立消费者按策略拉取重试”,否则单点故障会扩散成雪崩。Redis Stream 的 XREADGROUP + XACK + XCLAIM 是目前最轻量、最可控的落地方式。

















