Back to Scaling Go Applications guides

Routing in Go with Gorilla Mux

Ayooluwa Isaiah
Updated on April 16, 2025

When building web applications in Go, routing is one of the fundamental components you'll need to master.

While Go's standard library provides basic routing capabilities, most production applications require more sophisticated routing features.

This is where Gorilla Mux comes in. It's a powerful, flexible router that extends Go's native capabilities while maintaining its simplicity and performance.

The limitations of the Go standard library router

Go's standard library includes the net/http package, which provides basic routing capabilities. With the standard library, you can create simple routes like this:

 
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/about", aboutHandler)
    http.ListenAndServe(":8080", nil)
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the home page!")
}

func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "About us page")
}

To run this example, save it as main.go, then execute:

 
go run main.go

Then visit http://localhost:8080/ in your browser to see the home page content.

1.png

While the standard library router works for simple applications, it lacks features like path variables, pattern matching, HTTP method filtering, and convenient middleware support.

Gorilla Mux fills these gaps while maintaining compatibility with Go's standard interfaces.

Getting started with Gorilla Mux

Let's start by setting up a basic project using Gorilla Mux. First, create a new project directory and initialize a Go module:

 
mkdir gorilla-demo && cd gorilla-demo
 
go mod init github.com/<yourusername>/gorilla-demo

Next, install the Gorilla Mux package:

 
go get -u github.com/gorilla/mux

Now, create a simple application with Gorilla Mux. Create a file named main.go with the following content:

main.go
package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to our website!")
}

func aboutHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "About our company")
}

func main() {
    // Create a new router
    r := mux.NewRouter()

    // Register routes
    r.HandleFunc("/", homeHandler).Methods("GET")
    r.HandleFunc("/about", aboutHandler).Methods("GET")

    fmt.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Run the application:

 
go run main.go

Visit http://localhost:8080/ and http://localhost:8080/about to see the pages. Notice that we're now explicitly specifying that these routes should only match GET requests with .Methods("GET").

This is one of Gorilla Mux's key features: the ability to filter routes by HTTP method.

Basic routing concepts

Gorilla Mux extends Go's routing capabilities with URL patterns, path variables, and HTTP method filtering. Let's explore these concepts with practical examples.

URL patterns and path variables

One of Gorilla Mux's most powerful features is the ability to capture variables from URL paths:

 
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

// Product represents a product in our system
type Product struct {
    ID    string  `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

func getProductHandler(w http.ResponseWriter, r *http.Request) {
    // Extract the id from the URL path
    vars := mux.Vars(r)
    productID := vars["id"]

    // In a real app, you'd fetch from database
    product := Product{
        ID:    productID,
        Name:  "Sample Product",
        Price: 29.99,
    }

    // Send JSON response
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(product)
}

func main() {
    r := mux.NewRouter()

    // Route with a path variable
    r.HandleFunc("/products/{id}", getProductHandler).Methods("GET")

    fmt.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Run this example and visit http://localhost:8080/products/123 in your browser or use curl:

 
curl http://localhost:8080/products/123
Output
{"id":"123","name":"Sample Product","price":29.99}

You'll receive a JSON response with the product ID "123". The path variable {id} captures any value in that segment of the URL, and mux.Vars(r) retrieves these variables as a map.

HTTP method-specific routes

Gorilla Mux lets you register different handlers for the same path based on the HTTP method:

 
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func getProductsHandler(w http.ResponseWriter, r *http.Request) {
    products := []map[string]interface{}{
        {"id": "1", "name": "Laptop", "price": 999.99},
        {"id": "2", "name": "Smartphone", "price": 499.99},
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(products)
}

func createProductHandler(w http.ResponseWriter, r *http.Request) {
    // Read request body
    _, err := io.ReadAll(r.Body)
    if err != nil {
        http.Error(w, "Error reading request body", http.StatusBadRequest)
        return
    }

    // In a real app, you'd parse and validate the JSON

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated)
    w.Write([]byte(`{"message": "Product created successfully"}`))
}

func main() {
    r := mux.NewRouter()

    // Same path, different handlers based on HTTP method
    r.HandleFunc("/products", getProductsHandler).Methods("GET")
    r.HandleFunc("/products", createProductHandler).Methods("POST")

    fmt.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Test these routes with curl:

 
curl http://localhost:8080/products
Output
[{"id":"1","name":"Laptop","price":999.99},{"id":"2","name":"Smartphone","price":499.99}]
 
curl -X POST -H "Content-Type: application/json" -d '{"name":"New Product","price":19.99}' http://localhost:8080/products
Output
{"message": "Product created successfully"}

This pattern is particularly useful for RESTful APIs, where different HTTP methods represent different operations on the same resource.

Pattern matching with regular expressions

You can use regular expressions in path variables to constrain what they match:

 
package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func articleHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)

    fmt.Fprintf(w, "Article: %s\nDate: %s/%s\nCategory: %s",
        vars["slug"], vars["month"], vars["year"], vars["category"])
}

func userHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    fmt.Fprintf(w, "User profile: %s", vars["username"])
}

func main() {
    r := mux.NewRouter()

    // Year must be exactly 4 digits, month exactly 2 digits
    r.HandleFunc("/articles/{category}/{year:[0-9]{4}}/{month:[0-9]{2}}/{slug}",
        articleHandler).Methods("GET")

    // Username must be lowercase letters and numbers only
    r.HandleFunc("/users/{username:[a-z0-9]+}", userHandler).Methods("GET")

    fmt.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Test these routes with your browser or curl:

 
curl http://localhost:8080/articles/technology/2023/05/introduction-to-go # Valid article URL
Output
Article: introduction-to-go
Date: 05/2023
Category: technology
 
curl http://localhost:8080/users/john123 # Valid username
Output
User profile: john123
 
curl http://localhost:8080/users/John123 # Invalid username (contains uppercase)
Output
404 page not found

The first and second requests will succeed, but the third will result in a 404 Not Found response because the username doesn't match the specified pattern.

Subrouters for route grouping

As your application grows, organizing routes becomes essential. Gorilla Mux provides subrouters for this purpose:

subrouters.go
package main

import (
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

func apiGetUsersHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "API: Get users")
}

func apiCreateUserHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "API: Create user")
}

func adminDashboardHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Admin: Dashboard")
}

func adminUsersHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Admin: Users")
}

func main() {
    r := mux.NewRouter()

    // API subrouter
    api := r.PathPrefix("/api/v1").Subrouter()
    api.HandleFunc("/users", apiGetUsersHandler).Methods("GET")
    api.HandleFunc("/users", apiCreateUserHandler).Methods("POST")

    // Admin subrouter
    admin := r.PathPrefix("/admin").Subrouter()
    admin.HandleFunc("/dashboard", adminDashboardHandler).Methods("GET")
    admin.HandleFunc("/users", adminUsersHandler).Methods("GET")

    fmt.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Test these routes:

 
curl http://localhost:8080/api/v1/users
curl -X POST http://localhost:8080/api/v1/users
curl http://localhost:8080/admin/dashboard
curl http://localhost:8080/admin/users

Subrouters create clean URL structures and help organize your code by grouping related routes. The paths registered on a subrouter are relative to its prefix, making the code more readable and maintainable.

Middleware integration

Middleware functions in Gorilla Mux run before or after your normal route handlers, perfect for cross-cutting concerns:

middleware.go
package main

import (
    "fmt"
    "log"
    "net/http"
    "time"

    "github.com/gorilla/mux"
)

// LoggingMiddleware logs request details
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()

        // Call the next handler
        next.ServeHTTP(w, r)

        // Log after the request is processed
        log.Printf("%s %s took %s", r.Method, r.RequestURI, time.Since(start))
    })
}

// AuthMiddleware checks for a valid token
func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")

        if token != "secret-token" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }

        // Token is valid, call the next handler
        next.ServeHTTP(w, r)
    })
}

func publicHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Public resource")
}

func privateHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Private resource")
}

func main() {
    r := mux.NewRouter()

    // Apply logging middleware to all routes
    r.Use(loggingMiddleware)

    // Public routes
    r.HandleFunc("/public", publicHandler).Methods("GET")

    // Private routes with auth middleware
    private := r.PathPrefix("/private").Subrouter()
    private.Use(authMiddleware)
    private.HandleFunc("", privateHandler).Methods("GET")

    fmt.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Test these routes:

 
# Public route (no auth required)
curl http://localhost:8080/public

# Private route without auth token
curl http://localhost:8080/private

# Private route with auth token
curl -H "Authorization: secret-token" http://localhost:8080/private

The logging middleware runs for all requests, while the auth middleware only applies to the private subrouter. The middleware executes in the order it's registered, so requests to /private will be logged even if authentication fails.

Request validation and processing

Gorilla Mux simplifies handling different types of request data. Let's explore some common patterns. Here's a complete example of processing a JSON request:

 
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

type User struct {
    Username string `json:"username"`
    Email    string `json:"email"`
    Password string `json:"password,omitempty"`
}

func createUserHandler(w http.ResponseWriter, r *http.Request) {
    var user User

    // Decode JSON request
    decoder := json.NewDecoder(r.Body)
    if err := decoder.Decode(&user); err != nil {
        http.Error(w, "Invalid request format", http.StatusBadRequest)
        return
    }

    // Validate required fields
    if user.Username == "" || user.Email == "" || user.Password == "" {
        http.Error(w, "All fields are required", http.StatusBadRequest)
        return
    }

    // In a real app, you'd save the user to a database
    // For this example, we'll just return a success message

    // Create response
    response := map[string]string{
        "id":      "123", // In a real app, this would be the generated ID
        "message": "User created successfully",
    }

    // Send JSON response
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusCreated) // 201 Created
    json.NewEncoder(w).Encode(response)
}

func main() {
    r := mux.NewRouter()

    r.HandleFunc("/users", createUserHandler).Methods("POST")

    fmt.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Test this route:

 
curl -X POST -H "Content-Type: application/json" -d '{"username":"john_doe","email":"john@example.com","password":"secret123"}' http://localhost:8080/users
Output
{"id":"123","message":"User created successfully"}

The handler decodes the JSON request body into a Go struct, validates the required fields, and returns a JSON response with the appropriate status code. In a real application, you'd also add more validation and error handling.

Error handling

Consistent error handling improves API usability and maintainability. Here's how to implement custom error handling with Gorilla Mux:

error_handling.go
package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"

    "github.com/gorilla/mux"
)

// ErrorResponse represents a standardized error response
type ErrorResponse struct {
    Status  int    `json:"status"`
    Message string `json:"message"`
    Error   string `json:"error,omitempty"`
}

// SendError sends a standardized error response
func sendError(w http.ResponseWriter, status int, message string, err error) {
    response := ErrorResponse{
        Status:  status,
        Message: message,
    }

    if err != nil {
        response.Error = err.Error()
    }

    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(response)
}

// NotFoundHandler handles 404 errors
func notFoundHandler(w http.ResponseWriter, r *http.Request) {
    sendError(w, http.StatusNotFound, "The requested resource was not found", nil)
}

// RecoveryMiddleware recovers from panics
func recoveryMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if err := recover(); err != nil {
                // Log the error
                log.Printf("Panic recovered: %v", err)

                // Return error to client
                sendError(w, http.StatusInternalServerError,
                          "An unexpected error occurred", nil)
            }
        }()

        next.ServeHTTP(w, r)
    })
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    userID := vars["id"]

    // Simulate a "not found" error for user with ID "0"
    if userID == "0" {
        sendError(w, http.StatusNotFound, "User not found", nil)
        return
    }

    // Simulate a panic for user with ID "panic"
    if userID == "panic" {
        panic("Simulated panic")
    }

    // Normal response
    user := map[string]string{
        "id":       userID,
        "username": "john_doe",
        "email":    "john@example.com",
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(user)
}

func main() {
    r := mux.NewRouter()

    // Apply recovery middleware
    r.Use(recoveryMiddleware)

    // Register routes
    r.HandleFunc("/users/{id}", getUserHandler).Methods("GET")

    // Set custom 404 handler
    r.NotFoundHandler = http.HandlerFunc(notFoundHandler)

    fmt.Println("Server starting on port 8080...")
    log.Fatal(http.ListenAndServe(":8080", r))
}

Test these error handling scenarios:

 
curl http://localhost:8080/users/123 # Normal request
 
curl http://localhost:8080/users/0 # Not found user
 
curl http://localhost:8080/users/panic # Trigger panic (which will be recovered)
 
curl http://localhost:8080/nonexistent # Not found route

This example demonstrates three error handling patterns:

  1. Function-level error handling in getUserHandler for application-specific errors.
  2. A custom 404 handler for routes that don't match any registered pattern.
  3. A recovery middleware that catches panics and prevents the server from crashing.

These patterns provide consistent error responses across your application, improving the developer experience for API consumers.

Final thoughts

Gorilla Mux enhances Go's routing capabilities without straying from the language's philosophy of simplicity and practicality.

Its path variables, regex patterns, subrouters, and middleware support provide the tools needed for building sophisticated web applications while maintaining compatibility with Go's standard library.

The router's approach to organization and error handling promotes maintainable, robust code that scales with your application's complexity. Whether you're building a simple API or a complex web service, Gorilla Mux offers a solid foundation that grows with your needs.

As you work with Gorilla Mux, remember that it follows Go's convention of being explicit rather than magical. This might require a bit more code compared to some other web frameworks, but it results in more maintainable and understandable applications in the long run.

Author's avatar
Article by
Ayooluwa Isaiah
Ayo is a technical content manager at Better Stack. His passion is simplifying and communicating complex technical ideas effectively. His work was featured on several esteemed publications including LWN.net, Digital Ocean, and CSS-Tricks. When he's not writing or coding, he loves to travel, bike, and play tennis.
Got an article suggestion? Let us know
Next article
The Fundamentals of Error Handling in Go
Licensed under CC-BY-NC-SA

This work is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License.

Make your mark

Join the writer's program

Are you a developer and love writing and sharing your knowledge with the world? Join our guest writing program and get paid for writing amazing technical guides. We'll get them to the right readers that will appreciate them.

Write for us
Writer of the month
Marin Bezhanov
Marin is a software engineer and architect with a broad range of experience working...
Build on top of Better Stack

Write a script, app or project on top of Better Stack and share it with the world. Make a public repository and share it with us at our email.

community@betterstack.com

or submit a pull request and help us build better products for everyone.

See the full list of amazing projects on github