Go Router: Chi and Gorilla Mux Patterns

Go's standard library `net/http` package provides a functional but basic router. It lacks URL parameter extraction, proper RESTful route definitions, and sophisticated middleware chaining. While you...

Key Insights

  • Chi offers a more modern API with built-in middleware composition and better context handling, while Gorilla Mux provides battle-tested stability with a larger ecosystem but less ergonomic patterns
  • Both routers solve net/http’s lack of URL parameter extraction and advanced routing, but Chi’s middleware chaining is significantly cleaner than Gorilla’s adapter-based approach
  • For new projects, Chi is the better choice due to active maintenance and superior performance (30-40% faster in benchmarks), but Gorilla Mux remains viable for teams with existing investments

Introduction to Go HTTP Routing

Go’s standard library net/http package provides a functional but basic router. It lacks URL parameter extraction, proper RESTful route definitions, and sophisticated middleware chaining. While you can build production applications with just net/http, you’ll end up writing significant boilerplate.

// Standard library routing - limited and verbose
http.HandleFunc("/users/", func(w http.ResponseWriter, r *http.Request) {
    // Manual path parsing required
    id := strings.TrimPrefix(r.URL.Path, "/users/")
    if r.Method != http.MethodGet {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    // Handle request...
})

Third-party routers like Chi and Gorilla Mux eliminate this friction. Gorilla Mux has been the de facto standard for years, offering comprehensive routing features. Chi emerged as a lightweight, idiomatic alternative that embraces Go’s context package and provides cleaner middleware patterns.

Choose Gorilla Mux when you need maximum compatibility with existing tools or have legacy code. Choose Chi for new projects where performance and modern Go patterns matter.

Setting Up Chi and Gorilla Mux

Installation is straightforward for both frameworks:

# Chi
go get -u github.com/go-chi/chi/v5

# Gorilla Mux
go get -u github.com/gorilla/mux

Here’s a minimal working example for each:

// Chi implementation
package main

import (
    "net/http"
    "github.com/go-chi/chi/v5"
)

func main() {
    r := chi.NewRouter()
    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello from Chi"))
    })
    http.ListenAndServe(":8080", r)
}
// Gorilla Mux implementation
package main

import (
    "net/http"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello from Gorilla Mux"))
    }).Methods("GET")
    http.ListenAndServe(":8080", r)
}

The syntax differs slightly, but both accomplish the same goal. Chi uses method-specific functions (Get, Post, etc.), while Gorilla Mux uses Methods() to specify HTTP verbs.

Route Definition Patterns

URL parameters are where third-party routers shine. Both frameworks make extracting path variables trivial:

// Chi - RESTful API routes
r := chi.NewRouter()

r.Get("/users/{userID}", func(w http.ResponseWriter, r *http.Request) {
    userID := chi.URLParam(r, "userID")
    w.Write([]byte("User ID: " + userID))
})

r.Get("/posts/{postID}/comments/{commentID}", func(w http.ResponseWriter, r *http.Request) {
    postID := chi.URLParam(r, "postID")
    commentID := chi.URLParam(r, "commentID")
    // Handle nested resource...
})
// Gorilla Mux - equivalent implementation
r := mux.NewRouter()

r.HandleFunc("/users/{userID}", func(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userID := vars["userID"]
    w.Write([]byte("User ID: " + userID))
}).Methods("GET")

r.HandleFunc("/posts/{postID}/comments/{commentID}", func(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    postID := vars["postID"]
    commentID := vars["commentID"]
    // Handle nested resource...
}).Methods("GET")

Chi’s URLParam function is slightly cleaner than Gorilla’s Vars() map approach. For subrouters, Chi provides exceptional ergonomics:

// Chi subrouter pattern
r.Route("/api/v1", func(r chi.Router) {
    r.Route("/users", func(r chi.Router) {
        r.Get("/", listUsers)
        r.Post("/", createUser)
        r.Route("/{userID}", func(r chi.Router) {
            r.Get("/", getUser)
            r.Put("/", updateUser)
            r.Delete("/", deleteUser)
        })
    })
})
// Gorilla Mux subrouter pattern
api := r.PathPrefix("/api/v1").Subrouter()
users := api.PathPrefix("/users").Subrouter()
users.HandleFunc("/", listUsers).Methods("GET")
users.HandleFunc("/", createUser).Methods("POST")
users.HandleFunc("/{userID}", getUser).Methods("GET")
users.HandleFunc("/{userID}", updateUser).Methods("PUT")
users.HandleFunc("/{userID}", deleteUser).Methods("DELETE")

Chi’s Route() function creates cleaner, more readable route hierarchies.

Middleware Implementation

Middleware is where Chi dramatically outperforms Gorilla Mux in terms of developer experience. Chi’s middleware is just a function that wraps an http.Handler:

// Chi middleware - clean and composable
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

r := chi.NewRouter()
r.Use(LoggingMiddleware)
r.Use(chi.Recoverer) // Built-in panic recovery

// Route-specific middleware
r.Group(func(r chi.Router) {
    r.Use(AuthMiddleware)
    r.Get("/admin", adminHandler)
})
// Gorilla Mux middleware - requires adapter pattern
func LoggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

r := mux.NewRouter()
r.Use(LoggingMiddleware)

// Route-specific middleware requires subrouters
admin := r.PathPrefix("/admin").Subrouter()
admin.Use(AuthMiddleware)
admin.HandleFunc("/", adminHandler).Methods("GET")

Here’s a practical authentication middleware example:

func AuthMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        
        // Validate token and add user to context
        userID := validateToken(token) // Your validation logic
        ctx := context.WithValue(r.Context(), "userID", userID)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

This middleware works identically in both frameworks, but Chi’s Group() function makes selective application more elegant.

Advanced Routing Patterns

Both routers support regex constraints, but with different syntax:

// Chi - regex patterns
r.Get("/articles/{slug:[a-z-]+}", getArticle)
r.Get("/users/{id:[0-9]+}", getUser)

// Gorilla Mux - regex patterns
r.HandleFunc("/articles/{slug:[a-z-]+}", getArticle).Methods("GET")
r.HandleFunc("/users/{id:[0-9]+}", getUser).Methods("GET")

For modular architecture, Chi’s mount pattern is superior:

// Chi - mounting sub-applications
func NewUserRouter() chi.Router {
    r := chi.NewRouter()
    r.Get("/", listUsers)
    r.Post("/", createUser)
    r.Get("/{id}", getUser)
    return r
}

func main() {
    r := chi.NewRouter()
    r.Mount("/users", NewUserRouter())
    r.Mount("/posts", NewPostRouter())
    http.ListenAndServe(":8080", r)
}

This pattern enables true microservice-style modularity within a monolith.

Performance and Benchmarking

Chi consistently outperforms Gorilla Mux in benchmarks:

// Simple benchmark test
func BenchmarkChiRouter(b *testing.B) {
    r := chi.NewRouter()
    r.Get("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte(chi.URLParam(r, "id")))
    })
    
    req := httptest.NewRequest("GET", "/users/123", nil)
    w := httptest.NewRecorder()
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        r.ServeHTTP(w, req)
    }
}

func BenchmarkGorillaMuxRouter(b *testing.B) {
    r := mux.NewRouter()
    r.HandleFunc("/users/{id}", func(w http.ResponseWriter, r *http.Request) {
        vars := mux.Vars(r)
        w.Write([]byte(vars["id"]))
    }).Methods("GET")
    
    req := httptest.NewRequest("GET", "/users/123", nil)
    w := httptest.NewRecorder()
    
    b.ResetTimer()
    for i := 0; i < b.N; i++ {
        r.ServeHTTP(w, req)
    }
}

Typical results show Chi processing 30-40% more requests per second with lower memory allocation. For applications handling thousands of requests per second, this difference is meaningful.

Choose Chi when performance matters and you want modern Go patterns. Choose Gorilla Mux when you need ecosystem compatibility or have existing infrastructure built around it.

Best Practices and Migration Tips

Common pitfalls:

  1. Forgetting to specify HTTP methods - Always be explicit about which methods your routes accept
  2. Middleware ordering - Authentication should come before authorization; logging should be first
  3. Not using context properly - Store request-scoped data in context, not global variables

Migration from Gorilla Mux to Chi:

The transition is straightforward. Replace mux.Vars() with chi.URLParam(), convert Methods() calls to method-specific functions, and refactor subrouters to use Route() or Mount(). Most handler functions require zero changes since both use standard http.HandlerFunc signatures.

Production considerations:

  • Always use timeouts on your HTTP server
  • Implement graceful shutdown
  • Add request ID middleware for distributed tracing
  • Use structured logging with context values
srv := &http.Server{
    Addr:         ":8080",
    Handler:      r,
    ReadTimeout:  15 * time.Second,
    WriteTimeout: 15 * time.Second,
    IdleTimeout:  60 * time.Second,
}

Both Chi and Gorilla Mux are production-ready. Chi’s active development and superior performance make it the better choice for new projects, but Gorilla Mux remains a solid option with years of battle-testing behind it.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.