main.go
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970
package main

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

func writeJSON(w http.ResponseWriter, status int, v interface{}) {
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(status)
	_ = json.NewEncoder(w).Encode(v)
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, map[string]interface{}{"status": "ok"})
	})
	mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) {
		writeJSON(w, http.StatusOK, map[string]interface{}{"ready": true})
	})

	srv := &http.Server{
		Addr:              ":8080",
		Handler:           mux,
		ReadHeaderTimeout: 5 * time.Second,
		ReadTimeout:       10 * time.Second,
		WriteTimeout:      10 * time.Second,
		IdleTimeout:       60 * time.Second,
	}

	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	errCh := make(chan error, 1)
	go func() {
		log.Printf("listening on http://localhost%v", srv.Addr)
		if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
			errCh <- err
			return
		}
		errCh <- nil
	}()

	select {
	case err := <-errCh:
		if err != nil {
			log.Fatalf("server failed: %v", err)
		}
		return
	case <-ctx.Done():
		log.Printf("shutdown requested")
	}

	shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
	defer cancel()

	if err := srv.Shutdown(shutdownCtx); err != nil {
		log.Printf("graceful shutdown failed: %v", err)
		_ = srv.Close()
	}
	log.Printf("server stopped")
}

How It Works

Runs an HTTP server with sane timeouts and health endpoints, shutting down gracefully on OS signals with context cancellation.

Builds a ServeMux with health and readiness checks, configures read, write, and idle timeouts, launches the server in a goroutine, listens for interrupt signals, calls Shutdown to drain connections, and logs any shutdown errors.

Key Concepts

  • 1Health and readiness endpoints support Kubernetes or load balancer probes.
  • 2Timeout configuration defends against slow clients.
  • 3Context-aware shutdown ensures handlers finish before closing sockets.

When to Use This Pattern

  • Drop-in starter server for new services.
  • Kubernetes probe-friendly HTTP endpoints.
  • Demonstration of graceful exit for standard library servers.

Best Practices

  • Differentiate readiness versus liveness responses appropriately.
  • Set generous but bounded Shutdown timeouts.
  • Handle http.ErrServerClosed as a normal condition to avoid noisy logs.
Go Version1.16+
Difficultyintermediate
Production ReadyYes
Lines of Code70