A guide to understanding HTTP Request handling and processing in Go
I have been writing/learning Go for the past one month. It has been a pleasurable one month and has rekindled the joy i had when i wrote my Hello World program.
HTTP in all it’s majesty is made up of requests and responses, no matter what it has been frankensteined to look like. And this is as true as that at Go’s end.
In this post, i would be taking a dive into the standard
net/http package in other to explain the process of handling requests and returning responses in Go.
This blog post is written for new Gophers.
In languages like PHP and Ruby, we have the concepts of
Routers := Receives the request, then dispatch accordingly.
Controllers := Handles the dispatched HTTP request. A controller can either be an object or a closure.
What about Go ?
In Go, we have only Handlers. Seriously. There aren’t routers. Everything is an Handler. And this is only possible because Go is a very opinionated language. Standards are first citizens here.
So how does Request dispatching and Response transfer work
I talked about standards the other time, all Handlers must implement a certain interface from the
net/http standard library
Starting with responses, HTTP in Go relies on the interface described above
All handlers (take that as controllers for a second) MUST have that signature, i.e a
ServeHTTP method that must be called to handle the dispatched request.
ResponseWriter;s job is to write the header, data bytes while the
Request helps in inspecting the HTTP request.
Don’t worry about the code. The only thing of interest right now is we have a struct (object ?) called HelloWorld that has a
ServeHTTPmethod which handles both routes (which makes it a controller).
localhost:9999/hello/lanre should spit “Hello Lanre” while
localhost:9999/hello/doe should give use “Hello John doe”.
What about Routers ? What dispatches the request to a controller ?
Routers in Go are called Multiplexers or ServeMuxes. But regardless of all the overloaded names, they are nothing more than regular handlers. A router is an handler in the fact that it also satisfy the
http.Handler interface. That is they have a
The only difference between a handler and other handlers - middleware, controllers - is that this handler is some sort of a root handler. As a root handler, you get to attach other handlers to it. Then when it gets run i.e the
ServeHTTP method is called, it then dispatches the request to a registered handler (controller/middleware) interested in the route.
This even makes the idea of tinkering with a custom made router cool.
Go’s philosophy is batteries included hence the standard library comes with a router that can be utilized in any application. In fact, we already made use of it in the code block above
http package comes with the
HandleFunc functions (which we used above) that helps map routes to handlers.
As you can see from the code block above, the package level functions -
HandleFunc - are actually syntactic sugar for attaching routes to a ServeMux - one provided by
net/http. They actually defer to
It is starting to get a bit fuzzy and it seems like there is lot of autowiring here 0.
Basically, what Go does is instantiate a ServeMux for you.
var DefaultServeMux = &defaultServeMux. Without this,
http.Handle wouldn’t work since we do not have an instance of
Let’s have a look at a simple but contrived example in other to put together the pieces we have seen so far - handlers (controllers) and errm, handlers (router). I like ancient mythology of gods (Greek and Egyptian), so we would be building something of that sort.
You should open up a console
After which you make a request to
localhost:4000, i prefer to use curl. Since we have go running our app already, we need another console window - I use a tiling terminal (guake mode in Tilix)
The top left window is what you are looking for
While this is powerful enough to help build web applications, there are issues with the
DefaultServeMux that must be considered before taking it into production (which our startup sadly enough has done). This considerations have nothing to do with performance but usabilty. While i list them as drawbacks, there are workarounds for them and i show the process of removing this drawbacks.
- For every new route, a complementary
structmust be created. This can get pretty frustating and tiring.
The only exception here is when we decide to reuse the same struct in multiple routes as we did with the
The workaround :
That doesn’t compile right ? Handlers are supposed to have a
Well yes, but the Go team helped with a little bit of abstraction. Go has a HandlerFunc type that has the same signature as the
ServeHTTP method. Due to Go’s flexibility, types can have methods - even if the type is a string. With this in mind, the
HandlerFunc adapts itself with a
ServeHTTP method in which it just cleverly calls itself i.e the function you passed in.
HandleFunc method just adapts a function into a
When you call
HandleFunc, it justs adapts the function into a
- Lack of parametized routes (
Let’s assume we were in a rush to ship an MVP as soon as possible, then we shipped the above code as is. The code runs fine after which you share the link of your new company on Twitter. And all is fine for the next few minutes until someone - who likes mythology and knows REST- tries to get the details for a god with a specific id. Remember our
json response has an id field. He then sends an HTTP request to
Whoops!!! Same response with the
/users/path and they aren’t the same URL. Our startup has been broken.
This isn’t a bug. The
DefaultServeMux provided by
net/http doesn’t try to solve all problems and this is quite understandable 1. The way
DefaultServeMux works is this :
Routes matching are very strict. Like very strict. It only checks if the request path has a prefix as that which was registered on the router, discarding the remainder path._
Here is the implementation of how paths are matched :
To fix this, we would have to manually inspect the request path, get the id -
users/2 . If an id exists in the path, we check if we have a god with the id. If yes, respond with it’s details, else return a 404 HTTP error. With this checks, we also have to make sure the registered path
users/ still works as expected.
And for the 404 response if a god couldn’t be found
While this is a legit fix and would even work in a large app with complex routing requirements, it is just saner to make use of a third party package that integrates this functionality.
The one with a custom Multiplexer
After examining the flaws of the default router it would be in all best interest to make use of a third party package that provides the functionality we need. There are tons of them right now but here are the ones i find most interesting :
Below is an example of an application that makes use of
Below is what one of the handler might look like:
Note that while this is slightly difference in syntax from
func PostLogin(w http.ResponseWriter, r *http.Request). It actually does the same thing. This is just to open the oppurtunity for stuffs like dependency injection (of a DB connection, mailer or whatever have you) in your handlers.
Nothing has changed except for the fact that we create a new router(Handler), attach some routes and tell
net/http to make use of our router by
http.ListenAndServe(":3000", router). With the
DefaultServeMux implementation, we passed nil to
ListenAndServe as the second argument which instructs Go to make use of the default multiplexer.
The main point is if you pass a handler (router) to
ListenAndServe, it makes use of that, but if
nil is passed, it makes use of
I hope this post helps someone understand how HTTP requests are handled and processed in Go.
In the previous example with
pressly/chi, i made use of middleware. I hope to write about that sometime in the future - just remember that they are still Handlers.
Update : Blog post on middleware
 This is one of the reasons why i so much love Go. Go is written in Go. I can decide to take a look at packages i am interested in - for instance
net/http - and figure out how stuff works which is even why i could write this post in the first place
net/http isn’t trying to be the one true multiplexer as that i think would have a drastic effect on the community as other legitimate and better solutions might be looked down upon since they aren’t official.
Update :=> I was wrong as per
net/http not being the only true multiplexer, Russ Cox who works on the language has more to say about this here