Go Gorilla Mux
last modified October 30, 2026
In this article we show how to do request routing and dispatching in Go with Gorilla Mux. We cover methods, query and path parameters, subrouters, middleware, static files, custom error handlers and reverse URL generation.
Gorilla Mux is an HTTP request multiplexer that implements
http.Handler. Compared with the standard ServeMux it
adds regular expressions in path variables, route names, subrouters with their
own middleware, and reverse URL generation. The project is in maintenance mode;
the standard ServeMux gained method and wildcard patterns in Go
1.22, but Mux is still widely used in existing applications.
The examples use gorilla/mux v1.8.1 and were run with Go 1.27.0.
Creating a router
A router is created with mux.NewRouter and passed to
http.ListenAndServe as the handler. HandleFunc
registers a function for a path and returns a route that can be refined
further.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "Hello there!")
})
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
The package is installed with go get github.com/gorilla/mux and
imported as github.com/gorilla/mux. The router matches the exact
path /hello and calls the function for it.
$ curl -s localhost:8080/hello Hello there!
Matching HTTP methods
Methods restricts a route to one or more HTTP methods. A request
with another method to the same path is answered with 405 Method Not Allowed
instead of reaching the handler.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/submit", func(w http.ResponseWriter, req *http.Request) {
w.WriteHeader(http.StatusCreated)
fmt.Fprint(w, "created\n")
}).Methods("POST")
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
The handler writes the 201 status before the body. A GET to
/submit does not match the route, because the method matcher fails,
and Mux returns 405.
$ curl -s -X POST localhost:8080/submit
created
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/submit
405
Query parameters
Query parameters are not part of the route; the handler reads them from
req.URL.Query(). The Get method returns an empty
string for a missing key.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/greet", func(w http.ResponseWriter, req *http.Request) {
name := req.URL.Query().Get("name")
if name == "" {
name = "guest"
}
fmt.Fprintf(w, "Hello %s!\n", name)
})
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
The name is URL-decoded by Query, so
John%20Doe arrives as John Doe. Without the
parameter the default name is used.
$ curl -s 'localhost:8080/greet?name=John%20Doe' Hello John Doe! $ curl -s localhost:8080/greet Hello guest!
Path variables
Path variables are written in braces and retrieved with mux.Vars.
Without a pattern a variable matches everything up to the next slash.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/hello/{name}", func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
fmt.Fprintf(w, "Hello %s!\n", vars["name"])
})
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
mux.Vars returns a map[string]string with the names
used in the route pattern. The value may contain spaces, which the client has
to escape.
$ curl -s localhost:8080/hello/John%20Doe Hello John Doe!
Regular expressions in variables
A variable can be constrained with a regular expression after a colon. Routes are matched in the order they were registered, so the more specific pattern must come first.
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
type User struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
r := mux.NewRouter()
r.HandleFunc("/users/{id:[0-9]+}", func(w http.ResponseWriter, req *http.Request) {
vars := mux.Vars(req)
var id int
fmt.Sscan(vars["id"], &id)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(User{ID: id, Name: "John Doe"})
})
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
Only digits match the route; /users/john does not match and is
answered with 404. The handler converts the string to an integer, sets the
content type and encodes a struct that carries JSON tags.
$ curl -s localhost:8080/users/42
{"id":42,"name":"John Doe"}
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/users/john
404
Subrouters
A subrouter groups routes under a common prefix. PathPrefix
returns a route, and Subrouter creates a router that inherits the
prefix.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
api := r.PathPrefix("/api/v1").Subrouter()
api.HandleFunc("/status", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "ok\n")
})
api.HandleFunc("/version", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "1.0\n")
})
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
Both handlers are registered on api and are reachable below
/api/v1. A subrouter keeps the parent router's matchers, so the
prefix applies to every route added to it.
$ curl -s localhost:8080/api/v1/status ok $ curl -s localhost:8080/api/v1/version 1.0
Middleware
Use appends a middleware to a router. The middleware wraps the
next handler, so it can inspect the request, reject it, or measure the
response. On a subrouter it applies only to routes registered after the call.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func auth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Auth") != "secret" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
func main() {
r := mux.NewRouter()
admin := r.PathPrefix("/admin").Subrouter()
admin.Use(auth)
admin.HandleFunc("/dashboard", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "dashboard\n")
})
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
A middleware is an http.Handler that calls
next.ServeHTTP when the request is acceptable. The request without
the header stops before the dashboard handler and receives 401.
$ curl -s -o /dev/null -w '%{http_code}\n' localhost:8080/admin/dashboard
401
$ curl -s -H 'X-Auth: secret' localhost:8080/admin/dashboard
dashboard
Serving static files
Static files are served by an http.FileServer mounted on a path
prefix. http.StripPrefix removes the prefix before the file server
looks up the path in its directory.
package main
import (
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.PathPrefix("/static/").Handler(
http.StripPrefix("/static/", http.FileServer(http.Dir("public"))))
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
With public/index.html in place, a request for
/static/ returns that file, and /static/css/app.css
would map to public/css/app.css. PathPrefix takes a
handler, not a handler function.
$ curl -s localhost:8080/static/ <!DOCTYPE html> <html lang="en"> <head><title>Static page</title></head> <body><p>Static home page.</p></body> </html>
Custom 404 and 405 handlers
The router has NotFoundHandler and
MethodNotAllowedHandler fields. They replace the plain text
responses that Mux would otherwise send.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
r.HandleFunc("/hello", func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "Hello there!\n")
}).Methods("GET")
r.NotFoundHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "custom not found", http.StatusNotFound)
})
r.MethodNotAllowedHandler = http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
http.Error(w, "custom method not allowed", http.StatusMethodNotAllowed)
})
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
The fields must be set before the server starts. They are global for the router, whereas a subrouter can have its own handlers.
$ curl -s -w ' [%{http_code}]\n' localhost:8080/nope
custom not found
[404]
$ curl -s -X POST -w ' [%{http_code}]\n' localhost:8080/hello
custom method not allowed
[405]
$ curl -s -w ' [%{http_code}]\n' localhost:8080/hello
Hello there!
[200]
Reverse URL generation
A route can be named with Name and later asked for a URL with
URL, which fills the variables from key/value pairs. This keeps
links in one place instead of repeating path strings.
package main
import (
"fmt"
"log"
"net/http"
"github.com/gorilla/mux"
)
func main() {
r := mux.NewRouter()
article := r.HandleFunc("/articles/{category}/{id:[0-9]+}",
func(w http.ResponseWriter, req *http.Request) {
fmt.Fprint(w, "article\n")
}).Name("article")
r.HandleFunc("/link", func(w http.ResponseWriter, req *http.Request) {
u, err := article.URL("category", "go", "id", "42")
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
fmt.Fprintln(w, u)
})
log.Println("Listening on 127.0.0.1:8080")
log.Fatal(http.ListenAndServe("127.0.0.1:8080", r))
}
URL returns a *url.URL and an error for a missing
variable or a value that does not match the pattern. The generated path
contains the encoded values in the order of the route pattern.
$ curl -s localhost:8080/link /articles/go/42
Source
Gorilla Mux - Github page
Gorilla Mux - package reference
In this article we have shown how to route requests with Gorilla Mux: methods, query and path parameters, regular expressions, subrouters, middleware, static files, error handlers and reverse URL generation.
Author
List all Go tutorials.