Go's standard library provides everything you need to build a fast, correct webhook receiver — no external dependencies required.
Complete Implementation
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"os"
"strings"
"sync"
)
var (
webhookSecret = os.Getenv("WEBHOOK_SECRET")
processed = &sync.Map{}
)
func verifySignature(body []byte, sigHeader string) bool {
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
provided := strings.TrimPrefix(sigHeader, "sha256=")
return hmac.Equal([]byte(expected), []byte(provided))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read error", http.StatusBadRequest)
return
}
defer r.Body.Close()
sig := r.Header.Get("X-Hub-Signature-256")
if sig == "" || !verifySignature(body, sig) {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
eventID := r.Header.Get("X-GitHub-Delivery")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"ok":true}`))
go func() {
if _, loaded := processed.LoadOrStore(eventID, true); loaded {
return
}
var event map[string]interface{}
if err := json.Unmarshal(body, &event); err != nil {
return
}
log.Printf("Processing %s: action=%v", eventID, event["action"])
}()
}
func main() {
http.HandleFunc("/webhooks/github", webhookHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}Why Go Excels for Webhook Receivers
- Goroutines: trivial async processing — spin up a goroutine after returning 200
- sync.Map: concurrent-safe idempotency store
- io.ReadAll: gives you raw bytes before parsing
- Low memory: handles thousands of concurrent connections
Use WebhookWhisper to forward live webhooks to your local Go server during development.