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.
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:
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
{"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
[{"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
{"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
Article: introduction-to-go
Date: 05/2023
Category: technology
curl http://localhost:8080/users/john123 # Valid username
User profile: john123
curl http://localhost:8080/users/John123 # Invalid username (contains uppercase)
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:
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:
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
{"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:
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:
- Function-level error handling in
getUserHandler
for application-specific errors. - A custom 404 handler for routes that don't match any registered pattern.
- 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.
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
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.comor submit a pull request and help us build better products for everyone.
See the full list of amazing projects on github