| name | servemux |
| description | [Applies to: **/*.go] This rule file provides definitive guidelines for using Go's `http.ServeMux` effectively, leveraging Go 1.22+ features for robust, maintainable, and secure API development. |
| source | cursor_mdc |
servemux Best Practices
Go's http.ServeMux, especially with the enhancements in Go 1.22+, is the definitive choice for building performant and maintainable HTTP services. This guide outlines the best practices for its use.
1. Code Organization and Structure
Organize your application for clarity, testability, and scalability.
✅ GOOD: Centralized Mux Assembly, Dedicated Handlers
Create a single http.ServeMux instance in your application's entry point (cmd/server/main.go) and register handlers from a dedicated handlers package. Inject business logic into handlers via interfaces.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"yourproject/internal/handlers"
"yourproject/internal/service"
)
func main() {
logger := log.New(os.Stdout, "API: ", log.Ldate|log.Ltime|log.Lshortfile)
userService := service.NewUserService(logger)
mux := http.NewServeMux()
handlers.RegisterRoutes(mux, logger, userService)
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 120 * time.Second,
ErrorLog: logger,
}
go func() {
logger.Printf("Server starting on %s", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Could not listen on %s: %v\n", server.Addr, err)
}
}()
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
logger.Println("Server shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
logger.Fatalf("Server forced to shutdown: %v", err)
}
logger.Println("Server exited gracefully")
}
package handlers
import (
"encoding/json"
"log"
"net/http"
"strconv"
"yourproject/internal/service"
)
type UserService interface {
GetUser(id int) (*service.User, error)
CreateUser(user *service.User) error
}
type UserHandlers struct {
log *log.Logger
svc UserService
}
func RegisterRoutes(mux *http.ServeMux, logger *log.Logger, userService UserService) {
uh := &UserHandlers{
log: logger,
svc: userService,
}
mux.HandleFunc("GET /users/{id}", uh.GetUser)
mux.HandleFunc("POST /users", uh.CreateUser)
}
func (uh *UserHandlers) GetUser(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
uh.respondWithError(w, http.StatusBadRequest, "Invalid user ID")
return
}
user, err := uh.svc.GetUser(id)
if err != nil {
uh.respondWithError(w, http.StatusNotFound, "User not found")
return
}
uh.respondWithJSON(w, http.StatusOK, user)
}
func (uh *UserHandlers) CreateUser(w http.ResponseWriter, r *http.Request) {
var user service.User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
uh.respondWithError(w, http.StatusBadRequest, "Invalid request payload")
return
}
if err := uh.svc.CreateUser(&user); err != nil {
uh.respondWithError(w, http.StatusInternalServerError, "Failed to create user")
return
}
uh.respondWithJSON(w, http.StatusCreated, user)
}
func (uh *UserHandlers) respondWithError(w http.ResponseWriter, code int, message string) {
uh.respondWithJSON(w, code, map[string]string{"error": message})
}
func (uh *UserHandlers) respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
response, err := json.Marshal(payload)
if err != nil {
uh.log.Printf("Error marshaling JSON response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
w.Write(response)
}
❌ BAD: Global Mux, Logic in main, Repetitive Naming
Avoid using http.DefaultServeMux and polluting main.go with handler logic. Do not repeat package or receiver names in function signatures.
package main
import (
"fmt"
"log"
"net/http"
"strconv"
)
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
http.Error(w, "Invalid ID", http.StatusBadRequest)
return
}
fmt.Fprintf(w, "Getting user %d\n", id)
}
func main() {
http.HandleFunc("GET /users/{id}", GetUserHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
2. API Design and Routing (Go 1.22+ Features)
Leverage Go 1.22's enhanced http.ServeMux for clean, expressive routing.
✅ GOOD: Method Matching, Path Values, and Exact Matches
Use HTTP method prefixes, {name} wildcards for path values, and {$} for strict path matching.
mux.HandleFunc("GET /users/{id}", uh.GetUser)
mux.HandleFunc("POST /users", uh.CreateUser)
mux.HandleFunc("DELETE /users/{id}", uh.DeleteUser)
mux.HandleFunc("GET /healthz/{$}", uh.HealthCheck)
mux.HandleFunc("/files/", uh.FileServer)
❌ BAD: Manual Method Checking, Suboptimal Path Matching
Avoid checking r.Method inside handlers or using generic path patterns when specific methods are required.
mux.HandleFunc("/users/{id}", uh.UserHandlerGeneric)
mux.HandleFunc("/healthz/", uh.HealthCheck)
3. Error Handling and Response Encoding
Always return structured JSON error responses and use json.NewEncoder for output.
✅ GOOD: JSON Error Responses, json.NewEncoder
Define a consistent error response structure. Use json.NewEncoder for efficient and correct JSON output.
func (uh *UserHandlers) respondWithError(w http.ResponseWriter, code int, message string) {
uh.respondWithJSON(w, code, map[string]string{"error": message})
}
func (uh *UserHandlers) respondWithJSON(w http.ResponseWriter, code int, payload interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(code)
if err := json.NewEncoder(w).Encode(payload); err != nil {
uh.log.Printf("Error encoding JSON response: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
}
}
❌ BAD: Plain Text Errors, Manual JSON Marshaling
Avoid http.Error for API responses and manual json.Marshal followed by w.Write.
4. Logging
Implement clear, request-level logging using the standard log package or a lightweight wrapper.
✅ GOOD: Request-Level Logging
Log incoming requests and critical events within handlers.
func (uh *UserHandlers) GetUser(w http.ResponseWriter, r *http.Request) {
uh.log.Printf("INFO: Handling GET /users/%s from %s", r.PathValue("id"), r.RemoteAddr)
}
5. Testing Approaches
Write comprehensive unit tests for each handler using httptest.NewRecorder.
✅ GOOD: httptest.NewRecorder for Handler Unit Tests
Test handlers in isolation, validating status codes, headers, and response bodies.
package handlers_test
import (
"bytes"
"encoding/json"
"log"
"net/http"
"net/http/httptest"
"testing"
"yourproject/internal/handlers"
"yourproject/internal/service"
)
type MockUserService struct {
GetUserFn func(id int) (*service.User, error)
CreateUserFn func(user *service.User) error
}
func (m *MockUserService) GetUser(id int) (*service.User, error) {
return m.GetUserFn(id)
}
func (m *MockUserService) CreateUser(user *service.User) error {
return m.CreateUserFn(user)
}
func TestGetUser(t *testing.T) {
tests := []struct {
name string
userID string
mockGetUser func(id int) (*service.User, error)
expectedStatus int
expectedBody string
}{
{
name: "Valid User ID",
userID: ,
mockGetUser: (*service.User, ) {
&service.User{ID: , Name: },
},
expectedStatus: http.StatusOK,
expectedBody: ,
},
{
name: ,
userID: ,
mockGetUser: (*service.User, ) {
, service.ErrNotFound
},
expectedStatus: http.StatusNotFound,
expectedBody: ,
},
{
name: ,
userID: ,
mockGetUser: ,
expectedStatus: http.StatusBadRequest,
expectedBody: ,
},
}
_, tt := tests {
t.Run(tt.name, {
mockSvc := &MockUserService{GetUserFn: tt.mockGetUser}
mux := http.NewServeMux()
handlers.RegisterRoutes(mux, log.Default(), mockSvc)
req := httptest.NewRequest(http.MethodGet, +tt.userID, )
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)
rr.Code != tt.expectedStatus {
t.Errorf(, tt.expectedStatus, rr.Code)
}
actual []{}
expected []{}
json.Unmarshal(rr.Body.Bytes(), &actual)
json.Unmarshal([](tt.expectedBody), &expected)
!bytes.Equal(rr.Body.Bytes(), [](tt.expectedBody)) && !jsonEqual(actual, expected) {
t.Errorf(, tt.expectedBody, rr.Body.String())
}
})
}
}
{
aj, _ := json.Marshal(a)
bj, _ := json.Marshal(b)
bytes.Equal(aj, bj)
}
6. Security Best Practices
Prioritize security by avoiding global state and validating all inputs.
✅ GOOD: Use a Local http.ServeMux
Always instantiate your own http.ServeMux instance. This prevents third-party packages from inadvertently or maliciously registering routes on your server.
mux := http.NewServeMux()
http.ListenAndServe(":8080", mux)
❌ BAD: Relying on http.DefaultServeMux
Never pass nil to http.ListenAndServe() or use http.Handle/http.HandleFunc directly, as this exposes the global http.DefaultServeMux.
http.HandleFunc("/", homeHandler)
http.ListenAndServe(":8080", nil)
7. Common Pitfalls and Gotchas
Be aware of servemux's specific behaviors to avoid unexpected issues.
✅ GOOD: Understand Pattern Precedence and Conflicts
Go 1.22+ ServeMux panics on registration if patterns conflict. Design your routes to be unambiguous. Use {$} for exact matches to prevent subtree matching.
mux.HandleFunc("GET /tasks/{id}/status", handlerTaskStatus)
mux.HandleFunc("GET /tasks/{id}", handlerTask)
mux.HandleFunc("GET /admin/{$}", handlerAdminRoot)
❌ BAD: Ambiguous Patterns, Forgetting {$}