Go Health Check — Complete Implementation Guide
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
Not handling context cancellation -- Go health checks should respect request contexts. If the client cancels (e.g., Kubernetes probe timeout), stop processing.
Blocking on dependency checks -- A slow dependency check blocks the health endpoint. Always use timeouts and context deadlines for dependency checks.
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.
Not using sync.RWMutex for shared state -- If health state is set from one Goroutine and read from another, protect it with a mutex.
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
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.
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.
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.
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
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