· Techtribe team · Tutorials · 5 min read
Middlewares in Go
Learn about how to implement middlewares in Go from scratch!
Table of Contents
- What are middlewares
- Most common use cases for middlewares
- What are middlewares in go
- Interfaces in Go
- Using our Middlewares
- MiddlewareChain
- Recover and Logging Middleware
Hello, and welcome! Today, we’re going to learn about Middlewares in go and implement everything from scratch using only golang standard libraries
What are middlewares
First we need to define what is a middleware. This term exists in other areas of computer science, but in the context of webservers ( APIs ), a middleware is a way to handle common tasks that needs to happen between receiving a request from a client and sending the response back.
Most common use cases for middlewares
What are middlewares in go
First and foremost, we need to define what is a middleware in go.
Basically, a middleware is any function that takes as parameter an http.Handler and returns an http.Handler.
Let’s see how it would be a base structure of a middleware in a go code:
As we said before, a middleware simply put is a function that receives a handler and returns a Handler. So in our case the following code would be the outer structure of our middlewares:
// simple middleware structure
func StructureMiddleware(next http.Handler) http.Handler {
}
But we can’t use that function skeleton, because we need to be able to wrap more functionality inside our function, that’s why we need to use a HandleFunc
Inside this middleware structure we need to return a HandlerFunc
This might look a little too abstract at first. So let’s break it down on why we need to wrap a HandlerFunc inside our middleware.
To do that first we need to understand how Interfaces work in go.
Interfaces in Go
An interface is just a type that specifies a set of method signatures, which are essentially function declarations
That said, in go any type can use interfaces methods if it implements all methods declared by that interface
Now that we know that an interface is just a type that declares method signatures, and that any type can use interfaces methods, let’s see why using the HandleFunc type adapter is working.
If you came from a language like Java, at first glance you might be a little confused on golang interfaces usage, that’s because you need to know about the interfaces implicit satisfaction.
That’s a powerful feature from interfaces in Go, but we will cover it more in a separate video in the future.
You can check inside the file server.go from net/http package that the HandleFunc type implements all the methods in the Handler interface, as it satisfies this go interface rule we can use the HandleFunc as it would be a Handler
Now you might be wondering, why do we need to use the HandleFunc in the first place? Couldn’t we basically use the Handler type directly?
The HandleFunc is a type adapter, a type adapter basically converts everything with the same signature. In our case, the HandleFunc is used to convert ordinary functions into HTTP handlers.
So the http HandlerFunc is just converting a function into an handler
Using our middleware
Now that we understood how is the base structure of a middleware in go, and why we need to use a HandleFunc, let’s see how it would be a complete simple middleware
Inside our middleware logic, we need to call the ServeHttp method so the handler or middleware passed as parameter will be executed
Let’s spin up a simple API to test this functionality.
If we leave this like that, whenever we use a Middleware we would need to wrap the Handler inside it everytime, in a real world scenario that’s not really useful.
For this reason we need to make a MiddlewareChain, it will allow us to chain a bunch of middlewares together easily.
A MiddlewareChain, is a common pattern used to create and nest middlewares in go. A MiddlewareChain basically needs to receive as parameters:
A ResponseWriter
A pointer to the Request
Infinite ( variadic ) amount of middlewares as third params
We can also create a type for our Middlewares so we can reuse that anywhere we need to.
MiddlewareChain
One important thing to notice is that the http.Handler already receive a ResponseWriter and a pointer to *Request, we can write a simple MiddlewareChain as follows:
func MiddlewareChain(h http.Handler, middlewares ...func(http.Handler) http.Handler) http.Handler {
for _, m := range middlewares {
h = m(h)
}
return h
}
As you can see, we are receiving a http.Handler as a parameter and a bunch of middlewares as needed.
Recover and Logging Middleware
Now we will declare a simple and common middleware, the RecoverMiddleware, it will allow us to recover from any panics that might occur in one of our handlers. Happily, golang already have a built-in function for that, which is the recover() function
Let’s also make a simple logging middleware, it will be useful to know how to implement one since it’s used a lot.
// this middleware will log the request
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
startTime := time.Now()
log.Println("Request:", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
endTime := time.Since(startTime)
slog.Info("request logging", slog.String("method", r.Method), slog.String("path", r.URL.Path),
slog.Duration("duration", endTime))
})
}
The next step is to wrap our Hello World handler with all the middlewares we have just created inside our MiddlewareChain. Let’s implement a simple mainHandler for this purpose inside our main function
const port = ":4000"
func hwHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
mux := http.NewServeMux()
mainHandler := MiddlewareChain(http.HandlerFunc(hwHandler), RecoverMiddleware, LoggingMiddleware, StructureMiddleware)
mux.Handle("/", mainHandler)
fmt.Println("Starting server on port", port)
if err := http.ListenAndServe(port, mux); err != nil {
log.Fatal(err)
}
}
Let’s spin up our server and see if it’s working correctly: