Learn Go Middlewares by Examples

Centralize and reuse common functionalities with middlewares

Jonathan Seow
Geek Culture

--

Photo by Elaine Casap on Unsplash

Middlewares are one of the most important concepts in backend engineering. They are standalone, reusable pieces of software that link different systems together.

In web development, it is common to place one or many middlewares between the client and server. This essentially creates a bridge between data and user interfaces.

Each middleware acts independently on an HTTP request or response. The output of one middleware can be the input of another middleware. This forms a chain of middlewares.

Two middlewares between client and server

Why middlewares? In web applications, middlewares allow us to centralize and reuse common functionalities on each request or response. For example, you can have a middleware that logs every HTTP request.

To design Go middlewares, there are certain rules and patterns that we have to follow. This article will teach you the concepts of a Go middleware and create two of them for a simple application!

I assume you have Go installed and are familiar with Go’s interfaces, structs, and the http package.

Let’s get started! 🏃

Getting Started

Let us first create a directory to house our mini Go project. Open your terminal, navigate to your favorite location and run the command below. This creates our root directory named go-middleware .

$ mkdir go-middleware

Then, change into the directory and create a main.go file.

$ cd go-middleware
$ touch main.go

Time to start coding! Open your code editor and let’s add some boilerplate code to kickstart a basic Go HTTP server.

A simple HTTP server

I have declared a new ServeMux (a router in Go’s terminology) with the http.NewServeMux() function. Then, I registered the home function as a handler for any HTTP requests to the /home route. Finally, I started an HTTP server at localhost:4000 with the custom router.

Once you are done, at the terminal, run go run main.go and visit localhost:4000/home in your favorite browser. You should see a response as follows.

A simple response at /home

Perfect! As a side note, you might notice some Go tutorials never declare a custom ServeMux. Instead, they use Go’s DefaultServeMux as follows.

DefaultServeMux in main.go

Notice that the home function is registered directly with http.HandleFunc . Behind the scenes, Go creates a DefaultServeMux object in the http package as the default router and adds the handler to it.

The code above works, and it looks simpler. But, it is not a good practice because DefaultServeMux is a global variable in the http package. Any third-party packages can add malicious handlers to it, which introduces security risks.

Photo by Artem Maltsev on Unsplash

Anyway, I digress. Let’s move on.

Handler and ServeHTTP

Before we start creating Go middleware, it is good for us to know some theories. In particular, what exactly is a handler in Go?

In Go, a handler is just an object (struct) that satisfies the http.Handler interface.

type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}

When we register the home function as a handler using http.HandleFunc , Go transforms our function into a handler with a ServeHTTP method. This ServeHTTP method simply calls our original home function.

To demonstrate, you can imagine Go doing something as follows.

A Go handler with ServeHTTP method

Meanwhile, a ServeMux object (be it default or custom) is also a http.Handler . It executes its ServeHTTP method when it receives an HTTP request, which matches the URL request path with the appropriate handler.

If there is a match, ServeMux proceeds to call the ServeHTTP method of the handler and passes the request to it. The handler then executes its route handling logic and returns a response.

In a way, you can think of Go’s application routing as a chain of handlers with ServeHTTP methods called one after another.

A chain of ServeHTTP calls

Hence, to fit into the chain, a Go middleware must behave like a handler. It performs some logic before passing a request to the next handler by calling the handler’s ServeHTTP method!

The Pattern of Go Middleware

By using the ideas presented in the section above, we can finally showcase what a Go middleware looks like. The standard pattern of a Go middleware is as follows.

The standard pattern of a Go middleware

A lot is going on in the snippet above. Let’s break it down.

  1. goMiddleware is a function that accepts a next parameter of type http.Handler and returns another http.Handler .
  2. A function f is created inside goMiddleware . It takes in two parameters of type http.ResponseWriter and *http.Request , which are the signatures of a typical handler function.
  3. The middleware logic is contained inside f .
  4. f passes the request to the next handler by calling the handler’s ServeHTTP method.
  5. f is transformed and returned as a http.Handler with the http.HandlerFunc adapter.
  6. The returned f forms a closure over the next handler. Hence, it can still access the local next variable even after it is returned by goMiddleware .

In practice, it is uncommon to explicitly create a function f . Instead, we simply pass an anonymous function to http.HandlerFunc and return it.

Pass an anonymous handler function to HandlerFunc

Don’t worry if the code snippets above look confusing. It will become clearer as we create and visualize some custom middlewares for our application.

The main takeaway is that a Go middleware is a function that accepts the next handler in the request chain as a parameter. It returns a handler that performs some logic before executing the next handler in the chain.

Creating Go Middlewares

The middlewares we are about to create will be based on the book Let’s Go by Alex Edwards.

Hooray! Now that we know the right concepts and theories, things will be much easier from here onwards. In this section, we will attempt to create two middleware, one to log HTTP requests and another to add basic security response headers.

Go back to your terminal, make sure you are still at the project directory and run the command below.

$ touch middleware.go

This creates middleware.go that will house the two Go middleware we are about to create. Open your code editor and let’s get coding!

Source: Let’s Go by Alex Edwards

logRequestMiddleware logs the network address, protocol version, HTTP method, and request URL to the standard output. This information is available in Go’s http.Request object.

secureHeadersMiddleware sets two security headers in the response (X-XSS-Protection and X-Frame-Options) to defend against XSS and Clickjacking attacks.

Before we register our middlewares in main.go , it is important to know that the positioning of your middlewares affects the behavior of your application.

In particular, if we want a middleware to act on every HTTP request, we should place it before the ServeMux. In other words, we need to pass the ServeMux handler as an argument to our middleware.

An example of such middleware is the one we just created to log requests.

Middleware before ServeMux

On the other hand, if we want a middleware to act on specific routes, we need to place them after the ServeMux. We pass individual route handlers as arguments to our middleware to achieve this.

An example would be an authentication middleware to protect private routes.

Middleware after ServeMux

We want every request to be logged in our simple application and each response to be set with the basic security headers. Hence, both the middlewares should be placed before the ServeMux.

Let’s register our middlewares in main.go as follows.

Add middlewares at line 14

Notice how we chain the middlewares with ServeMux. mux is a handler and it is passed as an argument into secureHeadersMiddleware . This returns another handler, which is passed into logRequestMiddleware .

Visually, the request flow looks something as follows.

Middlewares to ServeMux to route handler

To show the second type of middleware positioning, I will use a hypothetical scenario. Let’s say, somewhere in the future, we add a /private route along with a handler function secret .

A middleware named checkAuthMiddleware is created to protect this route against unauthorized access. To register this middleware to only the /private route, we can do something as follows.

mux.Handle("/private", checkAuthMiddleware(http.HandlerFunc(secret)))

Again, http.HandlerFunc converts the secret function to a http.Handler . This is passed as an argument to checkAuthMiddleware . Notice that we used mux.Handle instead of mux.HandleFunc as we are working directly with a http.Handler .

ServeMux to middleware to secret handler

Quick Testing

Let us test and see if our application is working correctly as expected.

I will be using curl to send HTTP requests from a terminal and see the responses. It should be pre-installed on Linux and macOS machines. Otherwise, you can download and install it from here.

If you have been following along, shut down your server with Ctrl+C at the terminal. Then, start it again with the go run command.

$ go run .

Open another terminal window to run our curl commands. First, we shall test if the HTTP requests are logged properly. You can send a GET request to localhost:4000/home with the command below.

$ curl -X GET localhost:4000/home

In the terminal window where you started the server, you should see a log as follows. Note that your network address will be different from mine.

A request log

Perfect! Now, to test our second middleware, we add a -i flag to our curl command above. This makes curl show us the response headers.

$ curl -i -X GET localhost:4000/home

After you run the command, you should see something as follows.

Curl response

Do you see the X-Frame-Options and X-Xss-Protection headers?

Final Thoughts

If you made it here, pat yourself on the shoulder! You have learned all the basics you need to start working with Go middlewares.

Just a final note, you may notice that our application does not scale well with the number of middlewares. The chain of middlewares will be harder to maintain as it grows longer.

Fortunately, there are third-party packages that help us in managing middlewares. One of them is called Alice. I’ll leave it to you to check out how it works.

My name is Jonathan, and I am a backend software engineer in Shopee, one of the fastest-growing e-commerce companies in Southeast Asia. Shopee is expanding at an unprecedented speed, and we are looking for bright engineering talents to join us!

If you are an engineer who wants to deliver software at scale in a fast-paced environment, then Shopee is the ideal place for you. Feel free to leave a note here or find me on LinkedIn if you wish to learn more about the company and our available positions.

Peace! ✌️

--

--