直接用net/http发送钉钉消息容易出错,因webhook要求json体、正确content-type、加签或access_token,常见400/403/空响应;需手动设header、关闭resp.body、校验statuscode,并注意timestamp为毫秒级。

为什么直接用 net/http 发送钉钉消息容易出错
钉钉机器人 Webhook 要求 POST 请求带 JSON 体、Content-Type: application/json,且必须携带签名(加签模式)或 access_token(普通模式)。很多人直接拼接 URL + http.Post,结果遇到:400 错误(JSON 格式不合法)、403(签名过期或计算错误)、空响应(没读 resp.Body 导致连接复用异常)。Go 的 http.Client 默认不校验响应状态码,也不自动关闭 body,这些细节不处理,线上报警就静默失败。
实操建议:
- 始终用
http.NewRequest构造请求,手动设Header.Set("Content-Type", "application/json") - 发送后必须
defer resp.Body.Close(),否则连接池耗尽 - 检查
resp.StatusCode,非 2xx 应记录原始resp.Body内容用于排障 - 加签模式下,
timestamp必须是毫秒级字符串,不是time.Now().Unix()(那是秒)
如何设计可配置的机器人客户端结构体
不同环境(测试/生产)用不同机器人,不同业务线可能需要不同 at 策略(at 所有人 / at 指定人 / 不 at),硬编码 URL 或 token 会导致发布时改代码。推荐定义一个 DingTalkClient 结构体,把变量化为字段:
type DingTalkClient struct {
Client *http.Client
Endpoint string // Webhook URL,含 access_token 查询参数
Secret string // 加签用的 secret,为空则跳过签名
AtMobiles []string // 可选:要 at 的手机号列表
IsAtAll bool // 是否 at 所有人
}
关键点:
-
Endpoint应该是完整 URL(如"https://oapi.dingtalk.com/robot/send?access_token=xxx"),避免每次拼接 -
Secret非空时,所有请求自动追加timestamp和sign查询参数(注意:不是放 body 里) -
AtMobiles和IsAtAll在构造 message body 时才生效,与传输层解耦
如何生成合法的加签 sign 参数
钉钉加签规则是:sign = base64(hmac_sha256(timestamp + "\n" + secret, secret))。常见错误是:用错了哈希算法(用了 sha256 而非 hmac_sha256)、没拼 \n、timestamp 用秒而非毫秒、base64 编码后没 URL 安全转义(虽然钉钉目前容忍,但最好做)。
实操建议:
- 用
hmac.New+sha256.New,别用crypto/sha256直接算 -
timestamp必须调用time.Now().UnixMilli()(Go 1.17+),老版本用time.Now().UnixNano() / 1e6 - 拼接字符串是
fmt.Sprintf("%d\n%s", ts, secret),注意换行符是\n不是\r\n - sign 值需经
url.QueryEscape后再拼到 URL,防止特殊字符破坏查询参数
如何封装 SendText 并支持重试与上下文超时
网络抖动或钉钉限流(每分钟最多 20 条)会导致发送失败。不能只写一次 Do 就返回,应内置简单重试(最多 2 次)和 context 控制超时(建议 5s)。同时,message body 要支持动态 at,不能把 at 字段写死在结构体里。
示例调用方式:
err := client.SendText(context.WithTimeout(ctx, 5*time.Second), "服务异常:db timeout", []string{"13800138000"}, false)
内部逻辑要点:
- body 构造用
map[string]interface{},动态加"at"字段(含"atMobiles"和"isAtAll") - 重试前 sleep 200ms,避免雪崩式重发
- 若响应 status 是 429(Too Many Requests),应立即返回错误,不重试(说明已触发限流)
- 最终错误需包含原始 HTTP 状态码和响应体(
io.ReadAll(resp.Body)),方便定位是签名错还是内容错
加签、重试、context 超时、at 动态控制——这四点漏掉任一,模块上线后就会在凌晨三点默默失效。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











