Untangling Handlers and HandleFuncs in Go
Go is a fantastic language for writing backends for APIs and other web applications in. It’s super fast, scalable, type-safe, comes with testing tools out of box, and compiles into a single, statically-linked binary that removes the hassle of dependency management.
And, while they exist, one of my favorite parts of Go is it’s really not necesary to reach for a framework when building an application.
Go ships with a feature-rich standard library which includes packages for working with HTTP and many other networking protocols, data-driven HTML templating, SQL database support, various cryptographic algorithm implementations, and much more.
In this article we are going to take a look at the net/http package, specifically how to set up routes to respond to HTTP requests.
This can actually be done with very few lines of code.
And while overall it is pretty simple, it can also be a little confusing at first. There are Handlers and HandlerFuncs both provide ways to respond to HTTP requests, but also similarly named Handle and HandleFunc functions for registering handlers.
So let’s untangle each of these by discussing their respective purposes, and finish with a working example of each.
TL;DR⌗
-
For a type to be used as a Handler, it must implement a
ServeHTTP()
method as defined in thehttp.Handler
interface. -
More often than not, you’ll probably want to take the shorter approach of using a regular function (with the appropriate signature) which you can wrap with the
http.HandlerFunc()
adapter to transform it into a handler.
The http.Handler Type⌗
Handler
is an interface type. Similar to other object-oriented languages, Go has the concept of interfaces which are essentially contracts that define what behavior the implementing type has, without specifying how that behavior works.
Interfaces provide a powerful abstraction layer that allows developers to swap out an object of one type for an object of another type, so long as they both implement all the methods defined by the interface. In fact, all a type needs to do to implement an interface is to implent each of its methods. There is no implements
keyword in Go like in other languages.
Since interfaces are a basically way of saying “this object is able to do a thing”, their names often end in er
. Thus, in the http package we have a Handler
that can handle HTTP requests and a ResponseWriter
that can write data to HTTP responses.
Enough about interfaces though.
If handlers are what we need in order to handle HTTP requests, how do we create one?
Well, the interface definition tells us we need a type that implements a ServeHTTP()
method:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Inside this ServeHTTP method is where we place our logic that does things like reading headers or query string parameters from the request, possibly some other backend processing such as retrieving data from a cache or database, and finally responding with some data and/or HTML code to the client.
Note: Handlers may read from but should not directly modify the request object. If the handler is a middleware that needs to make some data/state available to subsequent handlers, the request’s context may be used. I’ll cover this technique in depth in a later tutorial.
Once a Handler returns, it signals the request is finished, meaning no further reads from the request body or writes to the response should occur.
The http.HandlerFunc Type⌗
type HandlerFunc func(ResponseWriter, *Request)
A HandlerFunc is simply any function whose signature matches the one above. It takes a ResponseWriter
and a pointer to a Request
as parameters, and has no return type.
It is an adapter, or basically a shortcut that allows you to bypass needing to create an object for each type of request your application needs to handle.
Notice how the function parameters are the same as the ServeHTTP()
method all Handler’s have?
That’s because under the hood, the adapter makes a ServeHTTP method available implicitly with this:
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
Registering Handlers⌗
To register handlers so that our HTTP server knows about and can use them we have the Handle()
and HandleFunc()
functions.
One last concept we need to touch on is the ServeMux type.
ServeMux is an HTTP request multiplexer. It matches the URL of each incoming request against a list of registered patterns and calls the handler for the pattern that most closely matches the URL.
It’s a fancy name for what is essentially a request router, albeit a pretty basic one.
Routing is one area where you may want to consider a third party library that provides more advanced capabilities such as support for URL parameters or restricting routes to specific HTTP verbs.
ServeMux
is also a Handler
, and is a practical example of where you may need an object with other methods/capabilities in addition to fulfilling the Handler interface rather than a more simple HandlerFunc
.
The HTTP package ships with a default instance of ServeMux built-in, so calling http.Handle()
or http.HandleFunc()
to register routes makes use of that default ServeMux under the hood.
Usually you’ll want to make your own though by calling http.NewServeMux()
, and passing that object to your HTTP server.
Both Handle()
and HandleFunc()
take a string for the URL path as the first parameter, followed by an instance of the respective type.
Example⌗
I know this has been a lot of theory up to this point so let’s focus on some code now.
We’ve been tasked with building a very basic API service for a hypothetical book store.
Our API needs a simple health check endpoint our monitoring service will use to verify the API’s availability.
Besides that, we must provide a /book
endpoint which should return some info about a book. In our overly simplified example, our book store only sells one book, so we won’t need to worry about making it dynamic. It’ll return the same book on every request.
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
// Create a server multiplexer where we will define the routes
// our server will respond to.
// ServeMux is a special kind of handler (meaning it also implements
// the `http.Handler` interface) which provides some basic functionality
// of a router.
mux := http.NewServeMux()
// Register the `handleHealthCheck()` function as a handler for
// requests to `/health`.
mux.HandleFunc("/health", handleHealthCheck)
// This route is redundant and unnecessary, but demonstrates how
// we can use the `http.HandlerFunc()` adapter to transform our
// function to a `http.Handler` when needed.
mux.Handle("/health2", http.HandlerFunc(handleHealthCheck))
// Create an instance of the Book type, defined further below.
b := Book{
Title: "For the Love of Go",
Author: "John Arundel",
Price: 39.95,
}
// Since the Book type has a ServeHTTP() method, it implements the
// `http.Handler` interface and can be used to handle requests to
// the `/book` path.
mux.Handle("/book", b)
fmt.Println("Starting server on port 8080....")
// Start an HTTP server that listens at the specified address (in this
// case, it is listening at port 8080 on all interfaces), using the
// ServeMux we created above as the handler.
log.Fatal(http.ListenAndServe(":8080", mux))
}
// Define a function of type `http.HandlerFunc` that simply replies "OK"
// to all requests.
func handleHealthCheck(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("OK"))
}
// Define a type that holds a few basic properties of a book.
type Book struct {
Title string
Author string
Price float64
}
// Implement the `http.Handler` interface by defining a ServeHTTP() method
// on the book type. This is what allows us to map an instance of Book to
// a route handler above on line 36.
func (b Book) ServeHTTP(w http.ResponseWriter, r *http.Request) {
desc := fmt.Sprintf("%s by %s. Price: $%.2f\n", b.Title, b.Author, b.Price)
w.Write([]byte(desc))
}
Now we can run go run .
in our project directory to start our application and test out our routes.
For the sake of demonstration, we’ve got 2 health check routes, each using the same handleHealthCheck()
HandlerFunc to respond. I’ve included /health2
route just as an example of how to use the http.HandlerFunc()
adpater.
brian@Brians-iMac ~/dev/gospace/handlers-article curl -i http://localhost:8080/health
HTTP/1.1 200 OK
Date: Tue, 02 May 2023 02:46:14 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8
OK%
brian@Brians-iMac ~/dev/gospace/handlers-article curl -i http://localhost:8080/health2
HTTP/1.1 200 OK
Date: Tue, 02 May 2023 02:46:18 GMT
Content-Length: 2
Content-Type: text/plain; charset=utf-8
OK%
And finally, we have a Book
type that satisfies the http.Handler
interface and therefore can be used as a route handler for the /book
endpoint.
brian@Brians-iMac ~/dev/gospace/handlers-article curl -i http://localhost:8080/book
HTTP/1.1 200 OK
Date: Tue, 02 May 2023 02:46:22 GMT
Content-Length: 50
Content-Type: text/plain; charset=utf-8
For the Love of Go by John Arundel. Price: $39.95