Skip to main content

Web Server Development Pro Tips

Graceful Shutdown

Abruptly stopping a server can interrupt in-progress requests. Using http.Server.Shutdown() stops accepting new requests while waiting for ongoing requests to complete.

package main

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

func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
// Simulate a slow request
time.Sleep(2 * time.Second)
w.Write([]byte("done"))
})

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

// Receive OS signals (SIGINT: Ctrl+C, SIGTERM: docker stop)
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

// Start server in background
go func() {
log.Println("Server started: :8080")
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()

// Wait for signal
<-ctx.Done()
log.Println("Shutdown signal received, starting graceful shutdown...")

// Complete in-progress requests within 30 second timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()

if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("Forced shutdown: %v", err)
}
log.Println("Server shut down gracefully")
}

Framework Selection Guide

SituationRecommendation
Want to use only standard librarynet/http
Need maximum performance (minimize routing overhead)Gin
Heavy middleware customizationEcho
Microservices, minimize dependenciesnet/http or Chi
REST + gRPC mixedConnect-Go
// net/http (Go 1.22+) - built-in pattern-based routing
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", handleGetUser) // method+path pattern
mux.HandleFunc("POST /users", handleCreateUser)

Timeout Configuration

srv := &http.Server{
// Maximum time allowed for client to send request headers
ReadHeaderTimeout: 5 * time.Second,
// Maximum time to read the full request (including body)
ReadTimeout: 10 * time.Second,
// Maximum time to write the response (set longer for streaming APIs)
WriteTimeout: 30 * time.Second,
// Maximum idle time for Keep-Alive connections
IdleTimeout: 120 * time.Second,
// Maximum size of request headers (default 1MB)
MaxHeaderBytes: 1 << 20,
}

Request Size Limiting

func limitBodyMiddleware(maxBytes int64) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Limit body size (read error occurs on exceed)
r.Body = http.MaxBytesReader(w, r.Body, maxBytes)
next.ServeHTTP(w, r)
})
}
}
// 10MB limit
handler := limitBodyMiddleware(10 << 20)(mux)

Security Headers Middleware

func securityHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Prevent MIME type sniffing
w.Header().Set("X-Content-Type-Options", "nosniff")
// Prevent clickjacking
w.Header().Set("X-Frame-Options", "DENY")
// Enable XSS filter (legacy browsers)
w.Header().Set("X-XSS-Protection", "1; mode=block")
// Enforce HTTPS (1 year)
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// CSP: only allow scripts from same origin
w.Header().Set("Content-Security-Policy", "default-src 'self'")
// Referrer policy
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
next.ServeHTTP(w, r)
})
}

Health Check Endpoint Pattern

package main

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

type HealthStatus struct {
Status string `json:"status"` // "ok", "degraded", "down"
Timestamp time.Time `json:"timestamp"`
Checks map[string]string `json:"checks"`
}

// /health - basic liveness check (for load balancer)
func livenessHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

// /ready - actual service readiness (includes DB, cache connections)
func readinessHandler(w http.ResponseWriter, r *http.Request) {
checks := map[string]string{}
status := "ok"

// Check DB connection (example)
// if err := db.Ping(); err != nil {
// checks["database"] = "down: " + err.Error()
// status = "down"
// } else {
// checks["database"] = "ok"
// }

checks["database"] = "ok"

code := http.StatusOK
if status == "down" {
code = http.StatusServiceUnavailable
}

w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
json.NewEncoder(w).Encode(HealthStatus{
Status: status,
Timestamp: time.Now(),
Checks: checks,
})
}

// /metrics - Prometheus format metrics (using prometheus/client_golang)
// Register promhttp.Handler() to serve metrics automatically

GOMAXPROCS and Connection Pool Tuning

import (
"runtime"
_ "go.uber.org/automaxprocs" // automatically reflect container CPU quota
)

func init() {
// automaxprocs sets this automatically in container environments
// To set explicitly:
// runtime.GOMAXPROCS(runtime.NumCPU())
_ = runtime.NumCPU()
}

// HTTP client connection pool
transport := &http.Transport{
MaxIdleConns: 100, // total max idle connections
MaxIdleConnsPerHost: 10, // max idle connections per host
MaxConnsPerHost: 50, // max connections per host
IdleConnTimeout: 90 * time.Second,
DisableCompression: false,
}