ZetCode

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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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.

main.go
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Go tutorials.