Skip to content

Go Health Check — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

In this tutorial, you will learn about Go Health Check. We cover key concepts, practical examples, and best practices to help you master this topic.

Go health checks leverage the standard library's net/http package and can be extended with gin middleware, gRPC health protocol, and custom dependency verification for production monitoring.

What You'll Learn

By the end of this tutorial, you will know how to implement health checks in Go using the standard library, gin framework, and gRPC, with proper Kubernetes probe integration.

Why It Matters

Go is the language of cloud infrastructure. Most cloud-native tools like Kubernetes, Docker, and Terraform are written in Go. Understanding Go health checks is essential for cloud-native development.

Real-World Use

DodaTech's API Gateway is written in Go with a /healthz endpoint that performs dependency checks on all 20 upstream services and returns aggregate health within 5 milliseconds.

Go Health Check Learning Path

flowchart LR
  A[Django Health Check] --> B[Go Health Check]
  B --> C[net/http]
  B --> D[gin]
  B --> E[gRPC]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Standard Library Health Check

Go's net/http package provides everything needed for basic health endpoints.

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "time"
)

type HealthResponse struct {
    Status    string `json:"status"`
    Uptime    int64  `json:"uptime"`
    Timestamp string `json:"timestamp"`
}

var startTime = time.Now()

func healthHandler(w http.ResponseWriter, r *http.Request) {
    response := HealthResponse{
        Status:    "ok",
        Uptime:    int64(time.Since(startTime).Seconds()),
        Timestamp: time.Now().UTC().Format(time.RFC3339),
    }

    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("Cache-Control", "no-cache")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(response)
}

func readyHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]bool{"ready": true})
}

func main() {
    http.HandleFunc("/healthz", healthHandler)
    http.HandleFunc("/readyz", readyHandler)
    log.Println("Health endpoints at /healthz and /readyz")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

// curl http://localhost:8080/healthz
// {"status":"ok","uptime":42,"timestamp":"2026-06-28T12:00:00Z"}

Gin Framework Health Check

Gin provides routing and middleware for more structured health check implementations.

package main

import (
    "net/http"
    "time"
    "github.com/gin-gonic/gin"
)

type HealthCheckHandler struct {
    startTime time.Time
    ready     bool
}

func NewHealthCheckHandler() *HealthCheckHandler {
    return &HealthCheckHandler{
        startTime: time.Now(),
        ready:     false,
    }
}

func (h *HealthCheckHandler) Liveness(c *gin.Context) {
    c.JSON(http.StatusOK, gin.H{
        "status":    "alive",
        "uptime":    int64(time.Since(h.startTime).Seconds()),
        "timestamp": time.Now().UTC().Format(time.RFC3339),
    })
}

func (h *HealthCheckHandler) Readiness(c *gin.Context) {
    if h.ready {
        c.JSON(http.StatusOK, gin.H{"ready": true})
    } else {
        c.JSON(http.StatusServiceUnavailable, gin.H{"ready": false})
    }
}

func (h *HealthCheckHandler) SetReady() {
    h.ready = true
}

func main() {
    r := gin.Default()
    health := NewHealthCheckHandler()

    r.GET("/healthz", health.Liveness)
    r.GET("/readyz", health.Readiness)

    go func() {
        time.Sleep(5 * time.Second)
        health.SetReady()
    }()

    r.Run(":8080")
}

gRPC Health Check Protocol

gRPC provides a standard health checking protocol defined in grpc.health.v1.

package main

import (
    "context"
    "log"
    "net"
    "time"
    "google.golang.org/grpc"
    "google.golang.org/grpc/health"
    healthpb "google.golang.org/grpc/health/grpc_health_v1"
)

type HealthServer struct {
    healthpb.UnimplementedHealthServer
    serviceStatus map[string]healthpb.HealthCheckResponse_ServingStatus
}

func NewHealthServer() *HealthServer {
    return &HealthServer{
        serviceStatus: make(map[string]healthpb.HealthCheckResponse_ServingStatus),
    }
}

func (s *HealthServer) Check(ctx context.Context, req *healthpb.HealthCheckRequest) (*healthpb.HealthCheckResponse, error) {
    service := req.GetService()
    status, exists := s.serviceStatus[service]

    if !exists {
        return &healthpb.HealthCheckResponse{
            Status: healthpb.HealthCheckResponse_SERVICE_UNKNOWN,
        }, nil
    }

    return &healthpb.HealthCheckResponse{Status: status}, nil
}

func (s *HealthServer) Watch(req *healthpb.HealthCheckRequest, stream healthpb.Health_WatchServer) error {
    for {
        status := s.serviceStatus[req.GetService()]
        if err := stream.Send(&healthpb.HealthCheckResponse{Status: status}); err != nil {
            return err
        }
        time.Sleep(5 * time.Second)
    }
}

func main() {
    grpcServer := grpc.NewServer()
    healthServer := NewHealthServer()
    healthpb.RegisterHealthServer(grpcServer, healthServer)

    lis, _ := net.Listen("tcp", ":50051")
    log.Println("gRPC health at :50051")
    grpcServer.Serve(lis)
}

Dependency Checking in Go

Check downstream dependencies with configurable timeouts.

package main

import (
    "database/sql"
    "encoding/json"
    "net/http"
    "time"
    _ "github.com/lib/pq"
)

type DependencyCheck struct {
    db     *sql.DB
    client *http.Client
}

func NewDependencyCheck(db *sql.DB) *DependencyCheck {
    return &DependencyCheck{
        db: db,
        client: &http.Client{Timeout: 3 * time.Second},
    }
}

func (dc *DependencyCheck) CheckDatabase() map[string]interface{} {
    start := time.Now()
    err := dc.db.QueryRow("SELECT 1").Scan(new(int))
    latency := time.Since(start).Milliseconds()

    if err != nil {
        return map[string]interface{}{
            "name":    "database",
            "healthy": false,
            "error":   err.Error(),
        }
    }
    return map[string]interface{}{
        "name":      "database",
        "healthy":   true,
        "latencyMs": latency,
    }
}

func (dc *DependencyCheck) CheckExternalAPI(url string) map[string]interface{} {
    start := time.Now()
    resp, err := dc.client.Get(url)
    latency := time.Since(start).Milliseconds()

    if err != nil {
        return map[string]interface{}{
            "name":    "external-api",
            "healthy": false,
            "error":   err.Error(),
        }
    }
    defer resp.Body.Close()

    return map[string]interface{}{
        "name":       "external-api",
        "healthy":    resp.StatusCode == http.StatusOK,
        "statusCode": resp.StatusCode,
        "latencyMs":  latency,
    }
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

func main() {
    http.HandleFunc("/healthz", healthHandler)
    http.ListenAndServe(":8080", nil)
}

Common Mistakes

  1. Not handling context cancellation -- Go health checks should respect request contexts. If the client cancels (e.g., Kubernetes probe timeout), stop processing.

  2. Blocking on dependency checks -- A slow dependency check blocks the health endpoint. Always use timeouts and context deadlines for dependency checks.

  3. Ignoring the gRPC health protocol -- Kubernetes supports gRPC probes natively. Using the standard gRPC health protocol is better than custom HTTP endpoints for gRPC services.

  4. Not using sync.RWMutex for shared state -- If health state is set from one Goroutine and read from another, protect it with a mutex.

  5. Returning 200 when the service is shutting down -- During graceful shutdown, readiness should return 503. Check a shutdown flag in the readiness handler.

Practice Questions

  1. What is the gRPC health checking protocol? A standard protocol defined in grpc.health.v1 that uses Check (unary) and Watch (streaming) RPCs to report service health.

  2. How do you add timeouts to Go HTTP health checks? Create an http.Client with a Timeout field, or use context.WithTimeout in the request.

  3. What package provides the standard health check interface in Go? The standard library net/http package. Gin and other frameworks add convenience but aren't required.

  4. Challenge: Implement a Go health check that verifies multiple dependencies in parallel and aggregates results.

func aggregateHealthCheck(db *sql.DB, cacheURL string) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        results := make(chan map[string]interface{}, 2)
        allHealthy := true

        go func() {
            dbResult := checkDatabase(db)
            results <- dbResult
        }()

        go func() {
            cacheResult := checkCache(cacheURL)
            results <- cacheResult
        }()

        var deps []map[string]interface{}
        for i := 0; i < 2; i++ {
            result := <-results
            deps = append(deps, result)
            if !result["healthy"].(bool) {
                allHealthy = false
            }
        }

        w.Header().Set("Content-Type", "application/json")
        if !allHealthy {
            w.WriteHeader(http.StatusServiceUnavailable)
        }
        json.NewEncoder(w).Encode(map[string]interface{}{
            "healthy": allHealthy,
            "dependencies": deps,
        })
    }
}

FAQ

Should I use gin or net/http for health checks?

Both work. gin adds routing and middleware convenience but net/http is simpler and has zero dependencies.

How do Kubernetes gRPC probes work?

Kubernetes 1.24+ supports gRPC probes natively. Configure them with the grpc port and service name.

Can I use the same health check for HTTP and gRPC?

Yes, but use separate endpoints. HTTP health on :8080 and gRPC health on :50051.

How do I handle health checks in Go microservices?

Each microservice implements its own health endpoint. An API gateway or service mesh aggregates them.

What is the standard health check port in Go?

No standard, but :8080 for HTTP and :50051 for gRPC are common conventions.

Mini Project

Build a Go service with health checks using the standard library, including dependency checking with parallel execution and proper context handling for Kubernetes probes.

package main

import (
    "context"
    "encoding/json"
    "net/http"
    "os"
    "os/signal"
    "time"
)

type HealthService struct {
    healthy bool
    startTime time.Time
}

func NewHealthService() *HealthService {
    return &HealthService{
        healthy:   true,
        startTime: time.Now(),
    }
}

func (hs *HealthService) Handler() http.Handler {
    mux := http.NewServeMux()
    mux.HandleFunc("/healthz", hs.liveness)
    mux.HandleFunc("/readyz", hs.readiness)
    return mux
}

func (hs *HealthService) liveness(w http.ResponseWriter, r *http.Request) {
    json.NewEncoder(w).Encode(map[string]interface{}{
        "status": "alive",
        "uptime": int64(time.Since(hs.startTime).Seconds()),
    })
}

func (hs *HealthService) readiness(w http.ResponseWriter, r *http.Request) {
    if hs.healthy {
        w.WriteHeader(http.StatusOK)
        json.NewEncoder(w).Encode(map[string]bool{"ready": true})
    } else {
        w.WriteHeader(http.StatusServiceUnavailable)
        json.NewEncoder(w).Encode(map[string]bool{"ready": false})
    }
}

What's Next

Now that you understand Go health checks, learn about Spring Boot Actuator health endpoints. Then explore Kubernetes probe configuration.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro