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

  1. For a type to be used as a Handler, it must implement a ServeHTTP() method as defined in the http.Handler interface.

  2. 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.

  3. Jump to the example

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