用 http.HandleFunc 快速启动 Webhook 接收端需:1. 注册 POST 路由并校验方法;2. 用 io.ReadAll 一次性读取 body;3. 立即用 hmac.Equal 验签;4. 仅返回 200 状态码;5. 业务逻辑必须异步执行。

怎么用 http.HandleFunc 快速启动一个 Webhook 接收端
Webhook 本质就是个 HTTP POST 接口,Golang 用标准库就能跑起来,不需要框架。关键不是“能不能”,而是“怎么防错、怎么验签、怎么不丢事件”。http.HandleFunc 是最轻量的入口,但默认不解析 JSON、不设超时、不处理重复请求——这些都得自己补。
常见错误:直接 json.NewDecoder(r.Body).Decode(&v) 后就返回 200,结果上游重试三次,你却没做幂等,数据库多插三条记录。
- 必须在
HandleFunc里加r.ParseForm()或手动读取r.Body(注意只能读一次) - 建议用
io.LimitReader(r.Body, 1024*1024)防止大包打爆内存 - 立刻调
http.Error(r, "bad request", http.StatusBadRequest)拒绝非法 Content-Type(比如不是application/json或application/x-www-form-urlencoded)
如何验证 Webhook 请求来源是否可信(签名/Token)
几乎所有生产级 Webhook 都带签名机制,比如 GitHub 用 X-Hub-Signature-256,Slack 用 X-Slack-Signature,Stripe 用 Stripe-Signature。不校验等于把接口裸奔在公网。
典型坑:用 hmac.NewSHA256 算完直接比对字符串,却忘了上游签名是 hex 编码,而 Go 的 hmac.Sum256().String() 返回的是 base64;或者没按文档要求拼接原始 payload(有些要保留换行,有些要 strip whitespace)。
立即学习“go语言免费学习笔记(深入)”;
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 先从 header 取签名值,比如
r.Header.Get("X-Hub-Signature-256") - 用
io.ReadAll(r.Body)一次性读出原始字节(之后不能再读),再用相同密钥和算法重新计算 - 用
hmac.Equal()比对,别用==(防时序攻击) - 如果上游提供 timestamp(如 Slack),还要检查
X-Slack-Request-Timestamp是否在 5 分钟内
为什么 http.Server 要显式设置 ReadTimeout 和 WriteTimeout
Webhook 不是用户交互接口,它被第三方系统调用,响应延迟会直接触发对方重试。默认无超时,一个卡住的 goroutine 就可能拖垮整个服务。
更隐蔽的问题:某些 SaaS 平台(如 Shopify)要求 Webhook 响应在 5 秒内完成,超时即判定失败并入重试队列;而你的 handler 里如果调了慢 DB 查询或未设 context timeout,很容易踩线。
-
ReadTimeout控制从 TCP 连接建立到请求头读完的时间,建议设为 5–10 秒 -
WriteTimeout控制从响应头写开始到 body 写完的时间,必须 ≤ 上游要求的超时阈值(常为 3–5 秒) - 务必搭配
context.WithTimeout用在业务逻辑里,比如db.QueryContext(ctx, ...) - 不要依赖
http.TimeoutHandler—— 它只 wrap handler,无法中断底层 net.Conn
怎么安全地把 Webhook 事件转给后台任务(避免阻塞 HTTP 响应)
收到事件后立刻返回 200 是硬性要求,但实际处理(比如更新订单状态、发通知、调第三方 API)不能卡在 HTTP handler 里。Go 的 goroutine 看似简单,但没管控会泄漏。
常见翻车:起个 go processEvent(v) 就不管了,结果 panic 没 recover,goroutine 永久挂起;或者并发太多把 DB 连接池打满。
- 用带缓冲的 channel + 固定 worker 数量(比如 10 个 goroutine)做简单队列,避免无限起协程
- 每个 worker 处理前加
defer func(){if r:=recover();r!=nil{log.Printf("panic: %v", r)}}() - 对关键操作(如 DB 更新)加重试逻辑,但注意别让失败事件无限重入队列
- 如果量大或需持久化,直接上
redis或sqlite做任务表,HTTP handler 只 insert,worker select for update
真正的难点不在接收,而在“收到后怎么不丢、不错、不重”。签名、超时、异步这三块漏掉任何一环,上线后就会变成定时报警器。

















