Web Handlers and Middleware in GoLang
This is a collection of approaches to write web handlers and middleware in Go. It’d be useful for those who know the basics of writing web services in Go and are now looking at more modular, cleaner, and a little more advanced coding techniques.
The full code for this is here: https://play.golang.org/p/Y2_3P0aBP4I. We will consider it piece by piece and incrementally. I’ve named the handler functions with an ‘h’ prefix. The now() function prints the current time.
A Typical, Simple Handler
We start with simple, typical handler functions as shown below.
func h1(w http.ResponseWriter, r *http.Request) {
fmt.Println(now() + "before")
fmt.Println("h1")
fmt.Println(now() + "after")
}func h2(w http.ResponseWriter, r *http.Request) {
fmt.Println(now() + "before")
fmt.Println("h2")
fmt.Println(now() + "after")
}func main() {
http.HandleFunc("/h1", h1)
http.HandleFunc("/h2", h2)
}
Output
$ curl localhost:8080/h1Aug 12 11:05:30 before
h1
Aug 12 11:05:30 after
In functions h1 and h2, we have a simple logging functionality which prints the time at which the execution entered the function and then exited it. Here, we are having to repeat the logging code in each function, which isn’t convenient.
Adding Middleware
Middleware allows you to write code that will always be executed for handlers, which allows for better modularization.
func logger(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println(now() + "before")
defer fmt.Println(now() + "after") f(w, r) // original function call
}
}
func h3(w http.ResponseWriter, r *http.Request) {
fmt.Println("h3")
}func main() {
http.HandleFunc("/h3", logger(h3)) // wrap the handler call
}
The function h3 (above) is much simpler and only has its core functionality. The logging functionality is moved into a separate wrapper function, logger. Since HandleFunc requires the second parameter to have a particular signature (w http.ResponseWriter, r *http.Request), our wrapping function returns a function with the same signature within which the original function is called. Henceforth, all functions that require the entry and exit time logging can be handled easily by reusing this middleware.
The output will be similar to when we called h1 and h2.
$ curl localhost:8080/h3Aug 12 11:10:51 before
h3
Aug 12 11:10:51 after
type HandlerFunc
In the previous listing, the function signature func logger(f func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) feels lengthy. To make it easier, there is a type definition in the net/http package.
type HandlerFunc func(ResponseWriter, *Request)
Therefore you can simplify the function signature as below, which makes the code cleaner. The output will be the same.
func logger2(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println(now() + "before")
defer fmt.Println(now() + "after") f(w, r)
}
}func h4(w http.ResponseWriter, r *http.Request) {
fmt.Println("h4")
}
ServeHTTP
We saw previously a type redefinition called HandlerFunc in net/http. This type has an attached method called ServeHTTP.
// in net/http
type HandlerFunc func(ResponseWriter, *Request)// ServeHTTP calls f(w, r).
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
As you can see, ServeHTTP simply calls the original handler function. Hence, in our code, it is almost equivalent to substitute the function call f(w, r) with h.ServeHTTP(w, r). We can therefore rewrite the code as shown below and it would still work similarly and give you similar output. (There is an extra step in the call stack though.)
// ServeHTTP in net/http
func usingServeHTTP(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println(now() + "before")
defer fmt.Println(now() + "after") h.ServeHTTP(w, r)
}
}
Personally, I have not taken to using ServeHTTP in this context as much. I don’t know if there is any particular advantage either. But I do see this code approach often, and therefore it is good to at least know what it means.
Authorization/Authentication and Code Forwarding Decisions
Given that we have control over the actual core function in our wrapper, we can now make decisions on whether to forward the call to the wrapped function or not. In the code below, we have added an authorization wrapper too.
func auth(f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.NotFound(w, r)
return
} f(w, r)
}
}func h6(w http.ResponseWriter, r *http.Request) {
fmt.Println("h6")
}func main() {
http.HandleFunc("/h6", logger2(auth(h6)))
}
In this very simple example, we’re merely checking whether the method is a GET method. If it is, we allow the wrapped method to be called. If not, we reject the request with a 404. It is usually here that we’d check for the session id and see if this particular request has permissions to call the wrapped function.
Output:
$ curl localhost:8080/h6
Aug 12 11:15:01 before
h6
Aug 12 11:15:01 after$ curl -X POST localhost:8080/h6
404 page not found
Passing Parameters
The handler functions we have used in functions h1 and h2 have a strict signature that accept a ResponseWriter and a *Request. We cannot send any other parameters to it. With wrapped functions we have control over the parameters passed in to our middleware. We can still, however, only return a function that matches the original handler signature.
func param(cfg string, f http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println(cfg) f(w, r)
}
}func h7(w http.ResponseWriter, r *http.Request) {
fmt.Println("h7")
}func main() {
http.HandleFunc("/h7", logger2(param("staging", h7)))
}
In the code above, we are passing in a parameter to our param middleware. Accessing the end point via curl gives me the below output.
Aug 12 11:10:00 before
staging
h7
Aug 12 11:10:00 after
Keeping Handler Resources Encapsulated
Usually as part of handling web requests, we need to access external resources like databases, config settings, etc. In our early simple implementations, we tend to keep this data global within the main package. There is a better way to do this.
In the code below, we are taking a dummy example where our environment and db connection is just a string (ideally we would have an actual sql.DB instance). Instead of having them as separate variables, we keep them together in a struct called config.
Our function h8() is defined on our config type. Remember that the core handler function has to have the same signature as we expect for all handler functions, but it can be attached to any type.
config.h8() returns a function that matches the required signature and therefore this is valid. Interestingly, our core handler function now has access to all the common data like the db connection and the environment settings because it is a method on the config type.
type config struct {
env string
dbConn string
}// handlers hang off a type that understands the environment/server details
func (c config) h8() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("h8 in " + c.env + " via " + c.dbConn)
}
}func main() {
cfg := config{"production", "mysql"}
http.HandleFunc("/h8", cfg.h8())
}
Output:
$ curl localhost:8080/h8
h8 in production via mysql
One Time Initializations
Wrapper functions are invoked when you pass them in http.HandleFunc() calls. This invocation is done only once. The function that it returns, though, is called each time the end point is hit by client requests. This means that we can do any one time initializations as part of our middleware function.
func h9() http.HandlerFunc {
val := time.Now().Format(time.StampNano)
return func(w http.ResponseWriter, r *http.Request) {
fmt.Println("h9: server was started at " + val)
}
}func main() {
http.HandleFunc("/h9", h9())
}
In the example above, we have set the time that the middleware was first invoked. Since h9() is a closure over the actual handler function, it will have access to the variables set in the scope of h9().
Output:
$ curl localhost:8080/h9
h9: server was started at Aug 12 11:09:56.947376000$ curl localhost:8080/h9
h9: server was started at Aug 12 11:09:56.947376000$ curl localhost:8080/h9
h9: server was started at Aug 12 11:09:56.947376000
Adding Multiple Routes and Middleware
As programs grow, there will be many endpoints added. It becomes difficult to ensure that everybody sets up the middleware calls correctly. In projects that I worked on, I was also not a big fan of a huge build up of route definitions in main(). To get around these issues, I had a single place where I would add all my route handlers with the necessary middleware.
The code here is a simplified version of that idea: https://play.golang.org/p/S0cwVCpAnij
type route struct {
methods []string
url string
handler func(http.ResponseWriter, *http.Request)
}var aRoutes = []route{
{[]string{"GET"}, "/a1", f1},
{[]string{"POST"}, "/a2", f1},
}var bRoutes = []route{
{[]string{"PUT", "POST"}, "/b1", f1},
{[]string{"POST"}, "/b2", f1},
}func main() {
// collect all the routes here
var allRoutes []route
allRoutes = append(allRoutes, aRoutes...)
allRoutes = append(allRoutes, bRoutes...) // add them to the muxer with all required middleware
r := mux.NewRouter()
for _, v := range allRoutes {
r.HandleFunc(v.url, logger(auth(v.handler))).Methods(v.methods...)
} http.ListenAndServe(":8080", r)
}
Edit: I wrote this post based on Mat Ryer’s posts and some of my own thoughts. I then see that Mat spoke about this at Gophercon EU 2019. The video is here: https://www.youtube.com/watch?v=8TLiGHJTlig. There are a couple of additional points I’d like to add from that — one that was new to me and one, again, where I’d come to my own variation of it previously.
WriteResponse
Writing an http response back is a common task. I found value in abstracting that into a single method. Mat in his video has shown a much smaller function that takes care of only JSON responses, but I used to have something similar to the one below where I could handle different response types.
func WriteResponse(w http.ResponseWriter, httpStatus int, data interface{}) {
switch v := data.(type) {
case string:
w.WriteHeader(httpStatus)
w.Write([]byte(v))
return
case []byte:
w.WriteHeader(httpStatus)
w.Write(v)
return
case error:
if httpStatus == 0 || httpStatus == http.StatusOK {
httpStatus = http.StatusInternalServerError
}
http.Error(w, v.Error(), httpStatus)
return
default:
js, err := json.Marshal(data)
if err != nil {
log.Printf("Could not marshal http write data: %+v", data)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(httpStatus)
w.Header().Set("Content-Type", "application/json")
w.Write(js)
return
}
}
Request and Response Data Types
This one was new and interesting for me and I intend to adopt it. One of the issues I saw was a proliferation of different types that are accepted in the request and sent in the response. Keeping it within the function means that we can reuse the same name in another handler function without having to invent unique names. Types that work with a particular handler are together and it also increases maintainability.
func (s *server) handleGreeting() http.HandlerFunc {
type request struct {
Name string
...
}type response struct {
Greeting string
...
}return func(w http.ResponseWriter, r *http.Request) {
// use request and response struct only within this function
}
}
References:
I’m on LinkedIn and Twitter. I help people and companies adopt and build solutions with GoLang, GCP, Angular, and Ionic. I conduct training on GoLang and am also a Google Cloud Authorized Trainer with all the current GCP technical certifications. I am looking to do more on Google Cloud across all their services.