Routing
Go 1.22+ stdlib routing with method-based patterns and middleware.
Go 1.22 added enhanced routing to http.NewServeMux() — method-based patterns, path parameters, and wildcard matching. No router library needed.
Basic Routing
mux := http.NewServeMux()
// Method + path pattern
mux.HandleFunc("GET /{$}", handleHome) // Exact root match
mux.HandleFunc("GET /game/{id}", handleGame) // Path parameter
mux.HandleFunc("POST /game/new", handleCreateGame)
mux.HandleFunc("GET /api/game/{id}/state", handleGameState)
Path parameters are accessed via r.PathValue():
func handleGame(w http.ResponseWriter, r *http.Request) {
gameID := r.PathValue("id")
// ...
}
Handler Structs
Group related handlers into a struct with dependencies injected:
type GameHandler struct {
gameSvc *service.GameService
sseHub *service.SSEHub
}
func NewGameHandler(gameSvc *service.GameService, sseHub *service.SSEHub) *GameHandler {
return &GameHandler{gameSvc: gameSvc, sseHub: sseHub}
}
func (h *GameHandler) CreateGame(w http.ResponseWriter, r *http.Request) {
game, err := h.gameSvc.CreateGame(r.Context())
if err != nil {
http.Error(w, "Failed to create game", 500)
return
}
http.Redirect(w, r, "/game/"+strconv.Itoa(int(game.ID)), http.StatusSeeOther)
}
Centralized Route Registration
Keep all routes in one file for visibility:
func Setup(mux *http.ServeMux, handlers *Handlers, auth *middleware.AuthMiddleware) {
// Public routes
mux.HandleFunc("GET /health", healthCheck)
mux.HandleFunc("GET /login", handlers.Auth.Login)
mux.HandleFunc("GET /login/callback", handlers.Auth.Callback)
// Public with optional auth (user populated if logged in)
mux.Handle("GET /{$}", auth.OptionalAuth(http.HandlerFunc(handlers.Template.Index)))
mux.Handle("GET /game/{id}", auth.OptionalAuth(http.HandlerFunc(handlers.Game.Show)))
// JSON API (for Alpine.js)
mux.Handle("GET /api/game/{id}/state", auth.OptionalAuth(http.HandlerFunc(handlers.Game.APIGetState)))
mux.Handle("POST /api/game/{id}/throw", auth.OptionalAuth(http.HandlerFunc(handlers.Game.APIThrow)))
// SSE streams (no auth wrapper needed)
mux.HandleFunc("GET /game/{id}/events", handlers.SSE.GameStream)
// Static files
mux.Handle("GET /static/", http.StripPrefix("/static/", http.FileServer(http.Dir("./static"))))
// Protected routes (require auth)
protected := http.NewServeMux()
protected.HandleFunc("GET /settings", handlers.Template.Settings)
protected.HandleFunc("POST /game/new", handlers.Game.CreateGame)
mux.Handle("/", auth.Auth(protected))
}
Protected Route Pattern
Use a nested ServeMux for auth-required routes:
// All routes registered on `protected` require authentication
protected := http.NewServeMux()
protected.HandleFunc("GET /dashboard", handleDashboard)
protected.HandleFunc("POST /game/new", handleCreateGame)
// Wrap with auth middleware and mount as catch-all
mux.Handle("/", auth.Auth(protected))
This is clean — unmatched public routes fall through to the protected mux, which checks auth before dispatching.
Gotchas
GET /{$}vsGET /— The{$}suffix means exact match. Without it,GET /matches everything.- Method is required for specificity.
mux.HandleFunc("/foo", h)matches all methods.mux.HandleFunc("GET /foo", h)only matches GET. - Path params are strings. Always parse and validate:
strconv.ParseInt(r.PathValue("id"), 10, 32). - Order doesn’t matter. The new mux picks the most specific pattern, not first-match.