Go by Example

HTTP Server

Use http.Server explicitly with ReadTimeout, WriteTimeout, and IdleTimeout - the zero-value server has no timeouts and is vulnerable to slowloris and connection exhaustion.

http.ListenAndServe starts an HTTP server using the package-level default mux and a zero-value server with no timeouts. For production use, construct http.Server explicitly so you control timeouts and can implement graceful shutdown.

Register handlers with http.HandleFunc (pattern, handler) or create a custom http.ServeMux. Go 1.22 added method and path-parameter syntax (GET /users/{id}) to the standard mux.

package main
 
import (
    "fmt"
    "net/http"
)
 
func main() {
    mux := http.NewServeMux()
 
    mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "hello, world")
    })
 
    // Go 1.22 path parameters: {id} is captured in r.PathValue("id")
    mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
        id := r.PathValue("id")
        fmt.Fprintf(w, "user: %s\n", id)
    })
 
    mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
        w.WriteHeader(http.StatusCreated)
        fmt.Fprintln(w, "created")
    })
 
    http.ListenAndServe(":8080", mux)
}

Construct http.Server explicitly to set timeouts, then implement graceful shutdown by listening for OS signals and calling server.Shutdown. In-flight requests are allowed to complete before the server stops accepting new connections.

package main
 
import (
    "context"
    "fmt"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)
 
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintln(w, "ok")
    })
 
    server := &http.Server{
        Addr:         ":8080",
        Handler:      mux,
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
        IdleTimeout:  120 * time.Second,
    }
 
    // Start serving in a goroutine
    go func() {
        fmt.Println("listening on :8080")
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            fmt.Fprintf(os.Stderr, "server error: %v\n", err)
            os.Exit(1)
        }
    }()
 
    // Block until SIGINT or SIGTERM
    quit := make(chan os.Signal, 1)
    signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
    <-quit
 
    // Give in-flight requests up to 30s to complete
    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()
    if err := server.Shutdown(ctx); err != nil {
        fmt.Fprintf(os.Stderr, "shutdown error: %v\n", err)
    }
    fmt.Println("server stopped")
}

In production

Always use http.Server explicitly rather than http.ListenAndServe - the convenience function creates a zero-value server with no timeouts, which is vulnerable to slowloris attacks (a client sends headers one byte per second, holding the connection open indefinitely) and connection exhaustion. Set ReadTimeout to limit how long the server waits for a complete request, WriteTimeout to limit the response write phase, and IdleTimeout to reclaim keep-alive connections from idle clients. For graceful shutdown, call server.Shutdown(ctx) in response to SIGTERM - Kubernetes sends SIGTERM before SIGKILL and waits terminationGracePeriodSeconds (default 30s). Shutdown stops accepting new connections, waits for active handlers to return, then closes the listener - returning nil on success. If handlers block indefinitely (missing context propagation), Shutdown will wait until the context deadline fires and then return an error, so make sure every handler respects context cancellation on slow paths.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free