When you make a request (as a client) to a server, series of processing are carried out on the said request, before you receive a responses. Some of these processing are done by the base application you are requesting the resources from, but it is most likely that during the request-response cycle several other applications might have interacted and made changes to your requests/response before you get a final result. These third-party applications are generally known as middlewares.
If you're a python/javascript developer you'd already be familiar with the term Middlewares as you probably get to interact with them when building apis or so.
Middlewares are basically decorated functions that wraps request handlers and provide additonal functionalities during the requests/response life cycle.
In django middlewares are added in a particular order into a list, which allows django to process them in a Top-Buttom approach when processing a requests,
and a Buttom-Top approach when returning a response.
In go on the other-hand, you can add a middleware by simply creating a function that recieves a handler as argument and returns it back to the http.handler method as an argument; what do i mean? let's experiment with a simple example:
package main
import (
"fmt"
"log"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
indexHandler := http.HandlerFunc(index)
server := &http.Server{
Addr: ":8000",
Handler: mux,
}
mux.Handle("/index", IndexLogger(indexHandler))
log.Printf("Serving http on %s", server.Addr)
log.Fatal(server.ListenAndServe())
}
func IndexLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
startTime := time.Now()
log.Printf("%s at %s", r.Method, r.URL.Path)
next.ServeHTTP(rw, r)
log.Printf("Finished %s in %v", r.URL.Path, time.Since(startTime))
})
}
func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "hello welcome to the index page")
}
In this simple example we've created a basic RequestLogger middleware that logs whenever we request for the index Uri.
when we run
go run main.go
and navigate to localhost:8000\index, you should see a similar output to:
$ go run main.go
2022/03/03 13:28:00 Serving http on :8000
2022/03/03 13:28:07 GET at /welcome
2022/03/03 13:28:07 Finished /welcome in 34.394µs
Note ServeHTTP calls the Handler's function within the current middleware's method body.
Exciting right? the functionalities we could include to our api-services with this is limitless, say we want to include this logger service to every registered route on our application, we would simply just wrap our desired function's handler with our indexLogger function, e.g:
func welcome(rw http.ResponseWriter, r *http.Request) {
fmt.Fprintf(rw, "Welcome to the welcome page")
}
// edit main function to include the welcome handler
WelcomeHandler := http.HandlerFunc(welcome)
mux.Handle("/welcome", IndexLogger(WelcomeHandler))
when we re-run our local server and navigate to /welcome we'd get a logged response on our terminal, indicating that the IndexLogger functionality was also added to the welcome URL Path, this is cool because not only does it allows similar behaviours within specific uri paths, but because it also supports the DRY(Don't Repeat Yourself) principle of Programming.
Chaining multiple http middlewares
Since in go a http middleware recieves a httpHandler and also returns a httpHandler, we can easily chain multiple middlewares that call each other before calling the base application/function.
For example, let's write another middleware function to check the Requests method of a URI path and return a method allowed
if request method is GET or not allowed if otherwise.
func CheckRequestMethod(next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
log.Printf("Only GET requests are accepted on the path!")
next.ServeHTTP(rw, r)
fmt.Fprintf(rw, "Method not allowed!")
} else {
log.Printf("Method allowed")
next.ServeHTTP(rw, r)
fmt.Fprintf(rw, "Your request is valid!")
log.Printf("Finished checking......")
}
})
}
// edit main function to wrap CheckRequestMethod on one of our registered routes.
mux.Handle("/welcome", CheckRequestMethod(IndexLogger(WelcomeHandler)))
when we re-run our local server and navigate to /welcome path, we'd notice that both CheckRequestMethod and IndexLogger have taken turns to act on our welcomeHandler before returning a response.
$ go run main.go
2022/03/03 19:57:29 Serving http on :8000
2022/03/03 19:57:41 Method allowed
2022/03/03 19:57:41 GET at /welcome
2022/03/03 19:57:41 Finished /welcome in 30.615µs
2022/03/03 19:57:41 Finished checking......
Flow of Chained Http Middlewares
Consider the following code:
func ProgramFlow() {
fmt.Println("First function body")
func() {
fmt.Println("Second function body")
func() {
fmt.Println("Third Function body")
time.Sleep(3 * time.Second)
fmt.Println("Finished third function body")
}()
fmt.Println("Finished second function body")
}()
fmt.Println("Finished first function body!")
}
When we call ProgramFlow() in our main Function, we get an output similar to
$ run main.go
First function body
Second function body
Third Function body
Finished third function body
Finished second function body
Finished first function body!
This is because in go each function is placed in it's own goroutine and it's output is first stored in a WaitGroup till all it's corresponding functions have returned before the it finally returns an output, similarly nested middlewares are treated in the same manner. When you navigate to \welcome;
- The webserver passes the control flow to CheckRequestMethod which logs a response depending on the requests method before executing the next handler.
- The next handler which is also a middleware logs the requests detail before executing the next handler.
- The main application is run and returns the normal response
- The IndexLogger logs it's final response
- The CheckRequestMethod logs it's final response
Third-Party MiddleWares in go
So far so good, we've been able to understand how middlewares work in go and also how you can write middlewares in your applications where need be. There are tons of libraries that implement several middleware components out-of-the-box for you, when building applications. So many have been created and are actively maintained to fit the best use cases, explore http://www.gorillatoolkit.org/ to see some already built handlers you could use for your next big projects.
Chaining Middlewares with the Alice Package
Alice is a go library that can be used to chain mulitple middleware functions and application handler together.
with Alice we do not need to pass each middleware handler as a parameter to the next, like we did in the above examples:
mux.Handle("/welcome",CheckRequestMethod(IndexLogger(WelcomeHandler)))
instead we could do
import "github.com/justinas/alice"
// edit our main function to create a variable that stores all our middlewares
allhandlers := alice.New(CheckRequestMethod, IndexLogger)
// add middlewares to registered route
mux.Handle("/welcome", allhandlers.Then(WelcomeHandler))
and just like we've chained all our middlewares with alice.
Conclusion
Middlewares are a great way to write re-usable pieces of code that can add great functionalities to whatever we are building, we only scratched the surface of what we could do with middlewares and in later articles we'd go into details on how to build applicable middlewares for our applications.
Meanwhile i'd suggest you go through more articles/videos on the subject matter to gain deeper understanding on how to go around building middlewares.
Let me know what you think about this article and possibly any amendments that could be made to improve it's user experience, thanks for reading and have a great time!
Top comments (0)