Paul Akinyemi I'm a fullstack web developer and technical writer with experience in JavaScript and Python. I love bringing pretty and complex user interfaces to life with clean and efficient code.

An intro to routing in Go with Gorilla Mux

9 min read 2776

An Intro To Routing In Go With Gorilla Mux

Editor’s note: This post was updated on 15 February 2023 to include information about matching routes, concurrency, as well as HttpRouter.

HTTP routers are tools and libraries that help receive and relay network requests to the specified handlers. HTTP routers run on servers, intercept incoming requests, and designate requests to specified handler functions.

Routers vary between backend frameworks; most backend frameworks ship with routers and many other functionalities for building software faster.

The Gorilla Mux package is one of the most popular routers and projects in the Go ecosystem, used in popular projects like Geth because of the package’s diverse features. Gorilla Mux provides functionalities for matching routes, serving static files, serving single-page applications (SPAs), middleware, handling CORS requests, and testing handlers.

N.B., as of December 2022, the entire Gorilla Web Toolkit was put into archive mode on GitHub. In the words of the project maintainers, this means that:

“All the repositories have gone into “read-only” mode. Anyone still using them can still clone them, go get them, and continue to build projects against them. In effect, there’s really no change here from the last 12 months, and it won’t break existing projects.
…What it does signal is that there will be no future development on these libraries.
…Folks are welcome to (as they always have been) fork them: all of the Gorilla libraries are permissively licensed (MIT, BSD-3, and Apache 2.0).”

You can read further details about why the toolkit was archived here.

This tutorial will walk you through using the Gorilla Mux package as a router for your applications. You’ll learn how to work with Gorilla Mux by using it to build a simple API.

Jump ahead:

Getting started with Gorilla Mux

Gorilla Mux

Once you’ve set up your Go workspace, run this command in your working directory to install the Gorilla Mux package:

go get -u github.com/gorilla/mux

After installing the Gorilla Mux package, import the packages and modules that you’ll be using in this tutorial at the top of your Go file, like so:

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

Gorilla Mux depends on the standard http package, and you’ll use the http package in many parts of this tutorial, including setting up a server. You’ll use the json package to encode and decode structs to JSON and vice versa.

Parsing structs into JSON in Go

Here’s the struct type you’ll use as the data model in this tutorial:

type Bio struct {
        Name string `json:"name"`
        Age  int    `json:"age"`
}

Gorilla Mux doesn’t provide functionality for parsing structs to JSON the way frameworks like Fiber do. Instead, in this tutorial you’ll be using the json package from the standard library to decode JSON requests and encode structs as JSON responses to clients.

Here’s an example of encoding and decoding with the json package. Start by creating the Go variable you want to decode:

var human Bio

The human variable is an instantiation of the Bio struct. You can use the Decode method of the Decoder struct returned by json.NewDecoder to parse the JSON body of an HTTP request into the initialized struct.

err := json.NewDecoder(request.Body).Decode(&human)
if err != nil {
  log.Fatalln("There was an error decoding the request body into the struct")
}

Similarly, you can use the Encode method of the Encoder struct returned by NewEncoder to write a struct that will be encoded as JSON to the client.

err = json.NewEncoder(writer).Encode(&human)
if err != nil {
  log.Fatalln("There was an error encoding the initialized struct")
}

Now that you’re familiar with the basics of working with JSON, let’s move on to routing.

Routing with the Gorilla Mux package

You can create a router instance with the NewRouter method, like so:

router := mux.NewRouter()

After declaring a new router instance, you can use the HandleFunc method of your router instance to assign routes to handler functions along with the request type that the handler function handles. Here’s an example:

router.HandleFunc("/api/v1/example", exampleHandler).Methods("GET")

The HandleFunc method assigned the api/v1/example route to the exampleHandler handler function to handle GET requests.

These are the router declarations for the endpoints of the CRUD API you’ll be building in this tutorial:

router.HandleFunc("/create", create).Methods("POST")
router.HandleFunc("/read", read).Methods("GET")
router.HandleFunc("/update", update).Methods("PUT")
router.HandleFunc("/delete", delete_).Methods("DELETE")

Your next task is to create these handler functions and set up a server.

Matching routes in Gorilla Mux

Before you go ahead to create your handlers, let’s pause and take a closer look at how you can match routes with Gorilla Mux. Right now your routing is based on the root of the application and the HTTP request method. For example, the declaration:

 router.HandleFunc("/create", create).Methods("POST")

We’ll call the create handler when a client makes a POST request to the address yourserveraddress.com/create. However, Gorilla Mux offers ways to be even more precise about which routes your endpoints match.



Let’s assume you wanted to ensure your create handler only processed requests made over HTTPS. All you’d need is to chain a scheme match to the route declaration, like so:

 router.HandleFunc("/create", create).Methods("POST").Schemes("https")

Gorilla Mux provides matches for several other criteria. For example, you can match routes by host URL, like this: router.HandleFunc("/route", handler).Host("www.yourdomain.com"). This declaration will only process requests to the URL www.yourdomain.com/route .

Mux also allows you to:

  • Match routes by path prefix
  • Match routes based on the presence of HTTP headers
  • Match routes based on query parameters in the request
  • Define custom matching functions for your needs

You can find examples using these matches here. A word of warning about matching routes with Mux: if an incoming request matches two or more route declarations (there’s a route conflict), the request will always be processed using the first route declaration.

Because applying the same matches to several different routes is tedious and can make your code harder to read, Gorilla Mux provides a feature called subrouting. This feature allows you to create a router that will apply a set of matches to all routes you register with it. Here’s an example:

router := mux.NewRouter()
s := router.Host("www.yourdomain.com").Headers("Connection", "Keep-Alive").Subrouter()
s.HandleFunc("/create", handler)
s.HanldeFunc("/update", updateHandler)

The /create and /update routes will now only be processed if the request has the appropriate Host and Headers. Now that you understand how route matching works, you can move on to creating your handlers.

Setting up handler functions

Your handler functions are where you’ll declare the business logic for your application. Depending on the operation, your handlers will need a writer object (to write a response to the client) and a request object (to obtain information about the incoming request).

These objects will typically be an instance of the http.ResponseWriter and/or *http.Request types, respectively. Here’s a skeletal example of a typical handler function that returns JSON responses to the client:

func example(writer http.ResponseWriter, request *http.Request) {
    writer.Header().Set("Content-Type", "application/json")
}

Your next step is to create the data store thatyou’ll use for this tutorial:

var BioData = make([]Bio, 0)

The BioData variable above is a slice of the Bio type you defined earlier. This data store is sufficient for this tutorial, but your projects will likely have more sophisticated needs that require you to use a database. If you aren’t familiar with working with databases in Go, you can check out the tutorials on MongoDB and the GORM ORM.

Now that your data store is in place, you can begin creating your request handlers. The create handler function is assigned to process POST requests, so its business logic will save the new JSON object in the request body to the application’s data store. Here’s the code for the create function:

func create(writer http.ResponseWriter, request *http.Request) {
        writer.Header().Set("Content-Type", "application/json")
        writer.WriteHeader(http.StatusOK)
        var human Bio
        err := json.NewDecoder(request.Body).Decode(&human)
        if err != nil {
            log.Fatalln("There was an error decoding the request body into the struct")
        }
        BioData = append(BioData, human)
        err = json.NewEncoder(writer).Encode(&human)
        if err != nil {
                log.Fatalln("There was an error encoding the initialized struct")
        }
}

The create handler function writes the StatusOk header to the client on request receipt, decodes the JSON request body into the human struct instance, saves the human struct to the BioData slice, and ends by writing the human struct as a response to the client.

Next up is the read handler. The read handler function is assigned to GET requests; thus, its business logic will fetch data from the data store and return matching data to the client based on the client’s request:

func read(writer http.ResponseWriter, request *http.Request) {
        writer.Header().Set("Content-Type", "application/json")
                name := mux.Vars(request)["name"]
        for _, structs := range BioData {
                if structs.Name == name {
                        err := json.NewEncoder(writer).Encode(&structs)
                        if err != nil {
                                log.Fatalln("There was an error encoding the initialized struct")
                        }
                }
        }

}

The read function works by reading the name parameter of the request using the Vars method of the mux package. It then loops through the BioData slice you’re using as a data store, and returns the struct that matches the name parameter to the client as JSON.

The update handler function is assigned to PUT requests, so its business logic should update the struct in the BioData data store specified by the request:

func update(writer http.ResponseWriter, request *http.Request) {
        writer.Header().Set("Content-Type", "application/json")
        var human Bio
        err := json.NewDecoder(request.Body).Decode(&human)
        if err != nil {
                log.Fatalln("There was an error decoding the request body into the struct")
        }
        for index, structs := range BioData {
                if structs.Name == human.Name {
                        BioData = append(BioData[:index], BioData[index+1:]...)
                }
        }
        BioData = append(BioData, human)
        err = json.NewEncoder(writer).Encode(&human)
        if err != nil {
                log.Fatalln("There was an error encoding the initialized struct")
        }
}

The update function parses the JSON in the request body into the human variable, loops through the BioData slice, deletes the entry if it exists, and finally appends the human struct from the request body to the BioData slice.

The delete_ handler function is assigned to DELETE requests; thus, its business logic will delete a specified struct from the data store:

func delete_(writer http.ResponseWriter, request *http.Request) {
        writer.Header().Set("Content-Type", "application/json")
        name := mux.Vars(request)["name"]
        indexChoice := 0
        for index, structs := range BioData {
                if structs.Name == name {
                        indexChoice = index
                }
        }
        BioData = append(BioData[:indexChoice], BioData[indexChoice+1:]...)
}

The delete_ function retrieves the name parameter from the request, loops through the BioData data store, and deletes the entry if it exists.

Now that you’ve set up your handler functions, the last step is to set up the server that will listen for requests.


More great articles from LogRocket:


Setting up a server

You can set up a server using the ListenAndServe method of the http package. The ListenAndServe method takes as arguments the port you want the server to run on and a router instance, if any. Here’s a server for your application:

func RunServer() {
        router := mux.NewRouter()
        router.HandleFunc("/create", create).Methods("POST")
        router.HandleFunc("/read", read).Methods("GET")
        router.HandleFunc("/update", update).Methods("PUT")
        router.HandleFunc("/delete", delete_).Methods("DELETE")

        err := http.ListenAndServe(":8080", router)
        if err != nil {
                log.Fatalln("There's an error with the server," err)
        }

}

Calling the RunServer function in the main function of your project should start up a server on the local host port 8080.

And, that’s all you need to know to get started building apps with Gorilla Mux!

Gorilla Mux and concurrency

Using Gorilla Mux doesn’t have much impact on the concurrency of your application compared to using the standard library’s ServeMux. Each handler function will still be called in a separate goroutine when processing an incoming request.

So what does this mean for you? It means you have to be careful when modifying data that’s shared between handlers, so you avoid race conditions. The easiest way to implement this is usually to avoid having any shared data in the first place and use only local variables. However, if shared data is something you can’t avoid, be sure to guard all access to the data with a Mutex or some other synchronization mechanism.

Gorilla Mux router vs. Chi router

Chi Router

Chi is a lightweight, composable router for building HTTP services in Go. You’ll find the Chi router useful for building large RESTful API services you want to maintain and support over time. Heroku, Cloudflare, and 99designs use the Chi router in production.

Chi is built on the context package, making it suitable for handling signaling, cancellation, and request-scoped operations across handler chains. The Chi package also contains sub-packages for middleware and generating documentation, and a rendering package for managing HTTP requests and response payloads.

Here’s a quick example of routing with the Chi router:

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

func main() {
        router := chi.NewRouter()
        router.Use(middleware.Logger)
        router.Get("/", func(writer http.ResponseWriter, request *http.Request) {
                writer.Write([]byte("welcome to the chi"))
        })
        http.ListenAndServe(":3000", router)
}

The main function starts up a server that listens on port 3000 and writes a string to the client as a response.

When you start a new project, you might wonder which router to use. Here’s a comparison between the two router packages to help you decide based on what you’re building:

Metric Gorilla Mux Chi Router
Speed Fast, see benchmarks Fast, see benchmarks
Documentation Generation No Yes
Popularity 17k stars, used by 77k projects on GitHub 12k stars, used by 11k projects on GitHub
Rendering Yes Yes
MiddleWare Yes Yes
WebSockets Yes Yes
Testing Yes Yes

The Gorilla Mux and Chi router are both great at routing, but you’ll find most Go developers use Gorilla Mux because it’s older and because there are more learning resources for Gorilla Mux.

Gorilla Mux router vs. HttpRouter

HttpRouter

HttpRouter is a fast, minimal router for Go applications that’s been around for a long time. It’s so fast and reliable that it’s used as the router of several Go web frameworks, the most popular of those being Gin. Here’s what the docs say about HttpRouter:

HttpRouter is a lightweight high performance HTTP request router (also called multiplexer or just mux for short) for Go.

In contrast to the default mux of Go’s net/http package, this router supports variables in the routing pattern and matches against the request method. It also scales better.

The router is optimized for high performance and a small memory footprint. It scales well even with very long paths and a large number of routes. A compressing dynamic trie (radix tree) structure is used for efficient matching

To give you a feel for how it works, here’s a tiny demo of routing with HttpRouter:

import (
   "github.com/julienschmidt/HttpRouter"
   "log"
   "net/http"
)

func main() {
   router := HttpRouter.New()
   router.GET("/", func(w http.ResponseWriter, r *http.Request, _ HttpRouter.Params) {
      w.Write([]byte("Hello from http router"))
   })

   log.Fatal(http.ListenAndServe(":8080", router))
}

Just like with Chi, in this demo the main function starts up a server that listens on port 8080 and writes a hello response to the client. To help you choose the best router for your needs, here’s a comparison between HttpRouter and Mux:

Metric Gorilla Mux HttpRouter
Speed Fast, see benchmarks Very fast, see benchmarks
Documentation Generation No No
Popularity 17k stars, used by 77k projects on GitHub 15k stars, used by 16k projects on GitHub
Rendering Yes Yes
MiddleWare Yes Yes
WebSockets Yes No
Testing Yes Yes

Gorilla Mux and HttpRouter are both excellent routers. HttpRouter has the advantage of speed and minimalism, but Mux offers more flexibility and functionality.

Conclusion

In this tutorial, you learned about the Gorilla Mux, HttpRouter, and Chi router packages, how to route and build APIs with the Gorilla Mux router, and how to evaluate the three packages to help you make better decisions for your projects.

Check out how these Go frameworks can help you build web applications faster.

Get setup with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID.
  2. Install LogRocket via NPM or script tag. LogRocket.init() must be called client-side, not server-side.
  3. $ npm i --save logrocket 

    // Code:

    import LogRocket from 'logrocket';
    LogRocket.init('app/id');
    Add to your HTML:

    <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script>
    <script>window.LogRocket && window.LogRocket.init('app/id');</script>
  4. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • ngrx middleware
    • Vuex plugin
Get started now
Paul Akinyemi I'm a fullstack web developer and technical writer with experience in JavaScript and Python. I love bringing pretty and complex user interfaces to life with clean and efficient code.

Leave a Reply