fmt, io, os, bufio, encoding/json, net/http, time, slog, regexp, flag, embed.
HTTP Middleware & Graceful Shutdown
The previous lesson built a tiny server. Production servers add three things on top: middleware (logging, auth, recovery), timeouts (so a slow client can't tie up a goroutine forever), and graceful shutdown (so in-flight requests finish when you deploy a new version).
Middleware as a function
A middleware is just a function func(http.Handler) http.Handler. The outer
handler wraps the next one and decides whether to call it.
The wrapping order matters: outer wrappers see requests before inner ones, and responses after. Recovery should usually be the innermost wrapper so a panic in a deeper middleware also gets caught.
Per-request values via context
Don't add fields to *http.Request; use the request context:
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.New().String()
ctx := context.WithValue(r.Context(), reqIDKey{}, id)
w.Header().Set("X-Request-ID", id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Downstream handlers read it with r.Context().Value(reqIDKey{}). Use a private
context-key type (type reqIDKey struct{}) to avoid collisions.
Server timeouts
The zero-value http.Server has no timeouts. A slow attacker (or a
disconnected mobile client) can park goroutines forever. Always set them:
srv := &http.Server{
Addr: ":8080",
Handler: handler,
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 90 * time.Second,
}
log.Fatal(srv.ListenAndServe())
ReadHeaderTimeout is the most important — it's the Slowloris defense and
should be set on every public server.
Graceful shutdown
When the deploy system sends SIGTERM, you want to: stop accepting new
connections, let in-flight requests finish (up to a deadline), then exit.
srv := &http.Server{Addr: ":8080", Handler: handler}
go func() {
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatal(err)
}
}()
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Println("shutdown error:", err)
}
srv.Shutdown(ctx) closes the listener, then waits for active handlers to
return — up to the context deadline.