Writing HTTP Handlers in Go
Pub. June 4, 2022
Handlers & ServeMux
When creating web services in Go, you essentially map routes to handlers, which then may contain all the logic necessary to process the incoming request or call additional functions. If you are familiar with some MVC frameworks like ExpressJS, you must have created controllers and routers to manage incoming requests. Our controllers in Go are called Handlers, and our router is called ServeMux. The job of the router is to match the URL of incoming requests against a list of predefined paths and call the controller attached to it.
Let’s have a look at a basic http handler example.
package main
import (
"net/http"
)
type todoHandler struct{}
func (th todoHandler) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("hey\n"))
}
func main() {
// Create a route mapping "/" to our handler
http.Handle("/todo", todoHandler{})
// Start the server on localhost port 8080.
http.ListenAndServe(":8080", nil)
}
To understand the code above, we need to touch on a very trivial yet crucial concept of Go, interfaces. Interfaces are basically function signatures, or simply put, must have methods
of any type in Go. Assuming we have a car type in our code base, we can define a car interface as follows.
type Car interface {
Horn()
Move()
Reverse() error
}
Without caring about the implementation of these signatures, any type in our system can be classified as a car as long as they have methods that match these signatures. This gives room for a lot of flexibility and improves the overall maintainability of our system.
Now back to our http handler example, if it is not obvious already, http.Handle is our router and it expects two parameters: the pattern and the handler. In the first paragraph, I mentioned that routers in Go are called ServeMux
and controllers are called Handlers
but in this example we haven’t created any ServeMux
because Go does that automatically for us under the hood. Unless explicitly defined, all handlers will be attached to the default ServeMux
. The main function could be rewritten like this:
func main(){
mux := http.NewServeMux()
mux.Handle("/todo", todoHandler{})
// Start the server on localhost port 8080.
http.ListenAndServe(":8080", mux)
}
The second param to the mux.Handle
is an interface type called Handler
with the following signature.
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Any type in our system that exposes the ServeHTTP method and fits this signature can therefore be used anywhere a handler is required, which is exactly what our todoHandler
type is doing.
In our app, we could implement handlers for each route, but it quickly becomes obvious that there must be a better way to do it, and there is.
HandleFunc & HandlerFunc
These two look and sound similar but are totally different types, HandlerFunc
is basically a function type that implements the ServeHTTP
interface, allowing us to use any function with the appropriate parameters in place of custom handlers such as the todoHandler
.
mux.Handle("/todo", http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("hey\n"))
}))
We can take it a step further by using the HandleFunc
method of ServeMux
mux.HandleFunc("/todo", func(rw http.ResponseWriter, r *http.Request) {
rw.Write([]byte("hey\n"))
})
Under the hood, all it does is convert the func
parameter into a HandlerFunc
and pass it to a mux.Handle
call like this:
func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
if handler == nil {
panic("http: nil handler")
}
mux.Handle(pattern, HandlerFunc(handler))
}
With these built-in methods, you can pretty much build a production-ready API and that for me is one of the beauties of Go. However, there are some really awesome packages with added functionalities that you could use to build more robust systems.
Here are some awesome ones I’ve used in no particular order.