go
67 lines · 8 steps
Building a health check endpoint in Go
An HTTP handler pings Postgres and Redis under a timeout and reports overall service health as JSON.
Explained by
highlit
1package health
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "time"
8
9 "github.com/redis/go-redis/v9"
10 "gorm.io/gorm"
11)
12
13type Checker struct {
14 DB *gorm.DB
15 Redis *redis.Client
16}
17
18type checkResult struct {
19 Status string `json:"status"`
20 Error string `json:"error,omitempty"`
21}
22
23type response struct {
24 Status string `json:"status"`
25 Checks map[string]checkResult `json:"checks"`
26}
27
28func (c *Checker) Handler(w http.ResponseWriter, r *http.Request) {
29 ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
30 defer cancel()
31
32 resp := response{Status: "ok", Checks: map[string]checkResult{}}
33
34 resp.Checks["postgres"] = c.pingDB(ctx)
35 resp.Checks["redis"] = c.pingRedis(ctx)
36
37 status := http.StatusOK
38 for _, res := range resp.Checks {
39 if res.Status != "ok" {
40 resp.Status = "degraded"
41 status = http.StatusServiceUnavailable
42 break
43 }
44 }
45
46 w.Header().Set("Content-Type", "application/json")
47 w.WriteHeader(status)
48 _ = json.NewEncoder(w).Encode(resp)
49}
50
51func (c *Checker) pingDB(ctx context.Context) checkResult {
52 sqlDB, err := c.DB.DB()
53 if err != nil {
54 return checkResult{Status: "down", Error: err.Error()}
55 }
56 if err := sqlDB.PingContext(ctx); err != nil {
57 return checkResult{Status: "down", Error: err.Error()}
58 }
59 return checkResult{Status: "ok"}
60}
61
62func (c *Checker) pingRedis(ctx context.Context) checkResult {
63 if err := c.Redis.Ping(ctx).Err(); err != nil {
64 return checkResult{Status: "down", Error: err.Error()}
65 }
66 return checkResult{Status: "ok"}
67}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Aggregating dependency probes into one endpoint gives orchestrators a single signal for readiness.
- 2Wrapping checks in a context timeout keeps a slow dependency from hanging the whole health request.
- 3Mapping any failed sub-check to a 503 lets load balancers route away from degraded instances.
Related explainers
go
package middleware import ( "crypto/sha256"
Deduplicating concurrent requests in Gin
singleflight
request-coalescing
middleware
Advanced
8 steps
go
package email import ( "errors"
Normalizing and deduping email addresses in Go
validation
normalization
deduplication
Intermediate
8 steps
go
type PostCursor struct { CreatedAt time.Time ID int64 }
Keyset pagination with cursors in Go
pagination
keyset-cursor
database
Intermediate
8 steps
go
package config import ( "fmt"
Parsing timeout config in Go
configuration
validation
error-wrapping
Intermediate
7 steps
go
func (h *WebhookHandler) HandleBatch(c *gin.Context) { var payload BatchWebhookPayload if err := c.ShouldBindJSON(&payload); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid payload"})
Handling batched webhooks in Gin
webhooks
signature-verification
job-queue
Intermediate
7 steps
go
package config import "time"
The functional options pattern in Go
functional-options
closures
immutability
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/building-a-health-check-endpoint-in-go-explained-go-0291/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.