Skip to content

Webhook 是什么?有哪些规范?如何用代码实现?(含 Golang 示例)

分布式高可用入口架构示意图

前言

你可能已经听说过 Webhook 这个词,也可能在 GitHub、Slack、钉钉或飞书的配置界面里见过它。它听起来像是一个“高级”的技术概念,但如果你从未实际用过,第一次接触时确实容易感到困惑——它到底是个什么东西?和普通的 API 调用有什么区别?为什么那么多平台都支持它?

这篇文章将从最基础的概念讲起,用通俗易懂的类比帮你理解 Webhook 的本质,然后深入探讨它目前有哪些“规范”(或者说是业界共识),最后用 Go 语言给出完整的代码示例,让你看完就能动手写代码。

一、Webhook 是什么?

1.1 一个生活化的类比

想象一下你去一家生意很火的餐厅吃饭。你点完餐后,有两种方式知道餐好了:

  • 方式一(轮询):你每隔 2 分钟就去柜台问一次:“我的餐好了吗?”——这就是轮询(Polling)。你不停地主动去问,浪费自己的时间,也打扰服务员。
  • 方式二(Webhook):你给服务员留下你的手机号,告诉 TA“餐好了打我电话”。然后你就可以安心坐着刷手机。等电话一响,你就知道可以去取餐了。——这就是 Webhook

Webhook 本质上就是 一种由事件驱动的 HTTP 回调机制。用更技术一点的话说:应用 A 在某个事件发生时,自动向应用 B 预先指定的 URL 发送一个 HTTP 请求(通常是 POST),并在请求体中携带相关数据。

简单一句话:Webhook 就是“反向 API”——不是你主动去要数据,而是数据源主动把数据推给你

1.2 Webhook vs. 传统 API

这是理解 Webhook 最关键的一步。

对比维度Webhook(事件驱动)传统 API(请求驱动)
通信方向推送(Push):数据源主动把数据发给你拉取(Pull):你主动向数据源请求数据
触发方式事件触发(代码提交、支付完成、新用户注册等)你的请求触发,需要定时或按需调用
实时性近乎实时,事件发生即刻通知取决于轮询频率,有延迟
资源消耗,只在有事件时才通信,需要频繁发送请求
典型场景GitHub 通知 CI/CD 流水线运行、支付回调通知订单系统查询天气、获取用户列表、提交表单

搞懂了这个区别,你就已经理解了 Webhook 最核心的设计思想。

二、Webhook 是如何工作的?

一个完整的 Webhook 交互包含三个核心步骤,可以用下面的时序图清晰地展示:

三个步骤详解:

① 事件触发 在源应用中,一个预设的事件发生了。常见的例子包括:

  • 有人在 GitHub 上 push 了代码
  • Stripe 上收到了一笔付款
  • 用户在新系统中完成了注册

② 发送 HTTP 请求 源应用根据你预先配置的 Webhook URL,构建一个 HTTP POST 请求,将事件相关的数据(Payload)放在请求体中发送出去。请求头中通常会包含用于验证的签名信息。

③ 接收与处理 你的应用在指定的 URL 上收到请求后,需要完成以下几件事:

  • 验证请求签名,确保请求确实来自合法的发送方
  • 解析请求体中的数据
  • 立即将任务投递到消息队列,然后返回 200 OK(避免因耗时操作导致超时)
  • 后台 Worker 异步消费任务,执行真正的业务逻辑(更新数据库、发送通知、触发后续流程等)

除了时序图,下面这张系统架构图可以展示 Webhook 在整个系统中的位置:

核心设计要点:

💡 为什么要在第三步立即返回 200 OK? Webhook 发送方通常有超时限制(如 GitHub 为 10 秒)和重试机制。如果你在 Webhook 处理函数中执行耗时操作(如复杂计算、多次数据库写入),一旦超过超时时间,发送方会认为请求失败并触发重试,导致重复处理。正确的做法是:验证签名后立即返回 200,将实际工作交给异步任务队列

三、Webhook 有哪些规范?

这是很多初学者会问的问题,答案是:目前没有一个被广泛接受的强制性全球标准

GitHub 的实现方式、Slack 的实现方式、钉钉的实现方式……每个平台在签名验证、重试机制、数据结构等细节上都有自己的做法。但这并不意味着没有“规范”可言——业界正在推动一些标准化草案,它们代表了未来的方向。

3.1 安全认证规范:SWT(Secure Webhook Token)

SWT 是 IETF(互联网工程任务组)提出的一个互联网草案,核心思路是使用一种专门用途的 JWT(JSON Web Token) 来验证 Webhook 请求的合法性。

它的关键设计包括:

  • 传输:强制使用 HTTPS + HTTP POST
  • Token 位置:将 SWT 放在请求头的 Authorization: Bearer <token> 字段,事件数据放在请求体。
  • 标准声明(Claims)
    • webhook.event:事件类型,如 payment.received
    • webhook.hash:请求体内容的加密哈希,保证数据完整性
    • webhook.retry_count:当前是第几次重试
    • expnbfiat:过期时间、生效时间、签发时间,防重放攻击
    • jti:JWT ID,防重放

遵循 SWT 可以让 Webhook 的认证达到企业级的安全水平。

3.2 流程控制规范:PCRP(Ping-Challenge-Resolve-Product)

这是另一个 IETF 草案,它关注的是 Webhook 交互的流程和生命周期管理,而不只是认证。

它通过一个自定义的 HTTP 头 X-PCRP-Type 来标识交互阶段:

  • ping:发送方检查接收方是否在线、是否准备好接收事件。
  • challenge:接收方发起验证(如 CAPTCHA 或 Token 校验),确认发送方身份。
  • resolve:发送方对 challenge 做出响应,提供验证凭证。
  • product:这是最终的事件数据,只有在 ping 和 challenge 都成功后才会发送。

PCRP 还定义了 X-PCRP-Transaction-ID(事务ID)、X-PCRP-Nonce(防重放)等头部,让 Webhook 交互像 TCP 三次握手一样可靠。

注意:SWT 和 PCRP 目前都还处于“Internet-Draft”阶段,尚未成为正式标准。但理解它们可以帮你设计出更可靠、更安全的 Webhook 系统。

四、如何用 Golang 实现 Webhook?

接下来我们用 Go 语言来实际写代码,分两个场景:

  1. 发送 Webhook:在事件发生时,主动调用第三方平台的 Webhook URL(比如向 Slack、钉钉、飞书发通知)。
  2. 接收 Webhook:搭建一个 HTTP 服务,作为接收方来处理别人发过来的 Webhook 请求。

4.1 场景一:发送 Webhook 到 Slack

Slack、钉钉、飞书都支持“入站 Webhook(Incoming Webhook)”——其实就是它们给你一个 URL,你往这个 URL 发 POST 请求,就能把消息推送到频道或群里。

以下是用 Slack 官方 SDK 发送消息的示例:

go
package main

import (
    "fmt"
    "log"

    "github.com/slack-go/slack"
)

func main() {
    // 你在 Slack 创建 Incoming Webhook 时获得的 URL
    webhookURL := "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"

    // 创建 Webhook 客户端
    wc := slack.NewWebhook(webhookURL)

    // 构造消息内容(支持 Slack Block Kit 丰富布局)
    msg := &slack.WebhookMessage{
        Text: "Hello! 这是一条来自 Go 应用的测试消息。",
        Blocks: []slack.Block{
            slack.NewSectionBlock(
                slack.NewTextBlockObject("mrkdwn", "*新版本已部署* :rocket:", false, false),
                nil,
                nil,
            ),
        },
    }

    // 发送
    if err := wc.Send(msg); err != nil {
        log.Fatalf("发送失败: %v", err)
    }

    fmt.Println("消息发送成功!")
}

对于钉钉和飞书,原理完全一样——你拿到它们提供的 Webhook URL,按照它们要求的 JSON 格式发 POST 请求即可。区别只在 JSON 结构体和签名算法上。

4.2 场景二:接收 GitHub 的 Webhook

作为接收方,你需要做的就是启动一个 HTTP 服务器,监听一个 POST 路由。但最关键的一步是验证请求的合法性——否则任何人都可以向你的接口发假请求。

以下是处理 GitHub Webhook 的完整示例:

go
package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"
    "os"
)

// GitHub 事件 Payload 结构(根据需要补充更多字段)
type GitHubPayload struct {
    Action     string `json:"action"`
    Repository struct {
        FullName string `json:"full_name"`
    } `json:"repository"`
    Sender struct {
        Login string `json:"login"`
    } `json:"sender"`
}

// 验证 GitHub 签名
func verifySignature(body []byte, signatureHeader string, secret string) bool {
    if signatureHeader == "" {
        return false
    }
    // GitHub 的签名格式: sha256=xxxxx
    expectedMAC := []byte(signatureHeader)
    // 实际需要解析出 sha256= 后面的部分,此处简化示意
    // 完整实现请参考: https://docs.github.com/zh/webhooks/securing
    h := hmac.New(sha256.New, []byte(secret))
    h.Write(body)
    expected := hex.EncodeToString(h.Sum(nil))
    // 注意:实际应使用恒定时间比较函数防止时序攻击
    return hmac.Equal([]byte("sha256="+expected), []byte(signatureHeader))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    // 1. 只接受 POST
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    // 2. 【关键】验证签名
    secret := os.Getenv("GITHUB_WEBHOOK_SECRET") // 从环境变量读取
    signature := r.Header.Get("X-Hub-Signature-256")

    body, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Failed to read body", http.StatusBadRequest)
        return
    }
    defer r.Body.Close()

    if !verifySignature(body, signature, secret) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }

    // 3. 解析 JSON
    var payload GitHubPayload
    if err := json.Unmarshal(body, &payload); err != nil {
        http.Error(w, "Invalid JSON", http.StatusBadRequest)
        return
    }

    // 4. 业务处理(此处建议异步处理,见下方最佳实践)
    log.Printf("收到事件: %s,触发者: %s,仓库: %s",
        payload.Action,
        payload.Sender.Login,
        payload.Repository.FullName,
    )

    // 5. 返回 200 确认
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "OK")
}

func main() {
    http.HandleFunc("/webhook/github", webhookHandler)
    log.Println("服务启动于 :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

五、最佳实践清单

无论你是用 Go 还是其他语言实现 Webhook,以下几点都是必须注意的:

实践项说明
始终使用 HTTPS生产环境必须用 HTTPS,保证数据在传输过程中不被窃听或篡改。
严格验证签名不要信任任何未经验证的请求。所有主流 Webhook 提供者都支持 HMAC 签名验证,这是你的第一道防线。
尽快返回 200 OK收到请求后,先验证签名,然后立即把任务丢进消息队列(如 Kafka、RabbitMQ)或异步任务队列,马上返回 200。不要在 Webhook 处理函数里做耗时操作,否则发送方可能超时并重试。
实现幂等性网络问题可能导致发送方重试同一个 Webhook 请求。你的处理逻辑必须能识别并忽略重复请求,例如通过检查事件 ID(X-GitHub-Deliveryjti)来去重。
完善的日志与监控记录所有收到的 Webhook 请求头、关键数据和错误信息。这在调试和问题排查时至关重要。

六、常见问题 FAQ

Q:Webhook 和回调(Callback)有什么区别?

A:两者本质上是同一件事——都是 HTTP 回调。但“Webhook”通常强调事件驱动自动化,而“Callback”更常用于描述异步操作完成后通知结果的场景。在实际使用中,这两个词经常混用。

Q:如果我的服务挂了,Webhook 会丢失吗?

A:大多数主流平台(GitHub、Stripe、Slack 等)都有重试机制,如果接收方返回非 2xx 状态码或超时,它们会按一定的退避策略重试多次(通常是几天内)。但如果你希望万无一失,应该自己做好消息持久化和补偿机制。

Q:所有的 Webhook 都用 JSON 吗?

A:JSON 是目前最主流的选择,但不是强制要求。也有平台使用 XML、Form Data 甚至纯文本。不过绝大多数情况下,你都会和 JSON 打交道。

七、总结

问题答案
Webhook 是什么?一种事件驱动的 HTTP 回调,数据源主动把数据推送给你,无需你轮询。
和 API 有什么不同?API 是你主动去拉取数据;Webhook 是别人主动把数据推给你。
有统一规范吗?目前无强制国际标准,但 IETF 有 SWT、PCRP 等草案;目前以各平台自己的实现为主。
如何用 Go 实现?发送方:构造 HTTP POST 请求到目标 URL;接收方:搭建 HTTP 服务,验证签名,异步处理业务。

Webhook 是一种极其强大的架构模式,它能让系统之间实现松耦合、事件驱动、近乎实时的通信。无论是构建 CI/CD 流水线、处理支付回调,还是对接各种 SaaS 服务,你都会频繁地和 Webhook 打交道。

希望这篇文章帮你彻底搞懂了 Webhook。

最后更新2026/06/22 16:06
如果你觉得这篇文章有帮助,或者想聊聊技术、工作,欢迎通过下面方式联系我:
contact fishfinal