Advanced Golang Tutorials: HTTP Middleware



Hi everyone,

In this post, I would like to talk about an important part of Web Development using Go: Middleware.

When you're building a web application there's probably some shared functionality that you want to run for many HTTP requests. You may want to log every request, compress every response, or check a cache before doing some heavy data processing. One way of achieving all these is to use Middleware.

Middleware, allow us to implement various functionalities around incoming HTTP requests to your server and outgoing responses. The most prevalent examples would be authentication, logging and tracing. Although there are many frameworks out there that provide this out of the box, I’d like to show how simple it is to create this logic using nothing but the standard library. Building your own implementation gives you maximum flexibility and control over the behaviour, reducing the need for external dependencies which can be difficult to manage. Let's take a look at a simple example to understand what a Middleware is and how to define it:

The Basic Principles

Making and using middleware in Go is pretty simple. We would like to:

  • Implement our middleware so that it satisfies the http.Handler interface. 
  • Create a chain of handlers containing both our middleware handler and our normal application handler, which we can register with a http.ServeMux.

Simple Example

As an example, let's create two main handlers. One for `home page`, and one for `about page`.

func homeEndpointHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "Hi there, I love Go! This is the home page.")
}

func aboutEndpointHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "This is the about page, ")
 fmt.Fprintf(w, "where you can find information about us.")
}
The next step would be creating the middlewares we would like to use:

// Logging middleware
func withLogging(next http.HandlerFunc) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  log.Printf("Logged connection from %s", r.RemoteAddr)
  next.ServeHTTP(w, r)
 }
}

// Tracing middleware
func withTracing(next http.HandlerFunc) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  log.Printf("Tracing request for %s", r.RequestURI)
  next.ServeHTTP(w, r)
 }
}
And this is how we would be using these Middleware in a HTTP server:

package main

import (
 "fmt"
 "log"
 "net/http"
)

func main() {
 http.Handle("/", withLogging(withTracing(homeEndpointHandler)))
 http.Handle("/about", withLogging(withTracing(aboutEndpointHandler)))

 if err := http.ListenAndServe(":8080", nil); err != nil {
  log.Fatal(err)
 }
}
This would work perfectly, however we have to nest all these middlewares each time we define a new endpoint. This is not the most practical. As an alternative, we should create a nestedMiddleware function where we can nest these middlewares. Here is the full example:

package main

import (
 "fmt"
 "log"
 "net/http"
)

// middleware provides a convenient mechanism for filtering HTTP requests
// entering the application. It returns a new handler which may perform various
// operations and should finish by calling the next HTTP handler.
type middleware func(next http.HandlerFunc) http.HandlerFunc

func main() {
 all := nestedMiddleware(withLogging, withTracing)
 http.Handle("/", all(homeEndpointHandler))
 http.Handle("/about", all(aboutEndpointHandler))

 if err := http.ListenAndServe(":8080", nil); err != nil {
  log.Fatal(err)
 }
}

func homeEndpointHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "Hi there, I love Go! This is the home page.")
}

func aboutEndpointHandler(w http.ResponseWriter, r *http.Request) {
 fmt.Fprintf(w, "This is the about page, ")
 fmt.Fprintf(w, "where you can find information about us.")
}

// Logging middleware
func withLogging(next http.HandlerFunc) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  log.Printf("Logged connection from %s", r.RemoteAddr)
  next.ServeHTTP(w, r)
 }
}

// Tracing middleware
func withTracing(next http.HandlerFunc) http.HandlerFunc {
 return func(w http.ResponseWriter, r *http.Request) {
  log.Printf("Tracing request for %s", r.RequestURI)
  next.ServeHTTP(w, r)
 }
}

// nestedMiddleware provides syntactic sugar to create a new middleware
// which will be the result of chaining the ones received as parameters.
func nestedMiddleware(mw ...middleware) middleware {
 return func(final http.HandlerFunc) http.HandlerFunc {
  return func(w http.ResponseWriter, r *http.Request) {
   last := final
   for i := len(mw) - 1; i >= 0; i-- {
    last = mw[i](last)
   }
   last(w, r)
  }
 }
}
We take each middleware function in the list in reverse order and pass its return value as an argument to the one before it, starting with the final handler. In this way, the chain happens in the order that it is passed in.

If we run the program and visit http://localhost:8080/, we get the following output in the console.

$ go run main.go
2019/01/19 11:46:03 Logged connection from [::1]:39836
2019/01/19 11:46:03 Tracing request for /
2019/01/19 11:45:56 Logged connection from [::1]:39836
2019/01/19 11:45:56 Tracing request for /about
As you can see, we can create as many Middleware as we want for our Endpoint handlers. This gives us the flexibility of manipulating, logging, tracing, authenticating incoming requests on the fly, as they arrive.

That's all for this post and if you have any questions or comments feel free to drop them below. Thanks!
Author:

Software Developer, Codemio Admin

Disqus Comments Loading..