mediocre-blog/_posts/2015-07-15-go-http.md
2018-01-18 13:59:57 +00:00

15 KiB

title
Go's http package by example

Go's http package has turned into one of my favorite things about the Go programming language. Initially it appears to be somewhat complex, but in reality it can be broken down into a couple of simple components that are extremely flexible in how they can be used. This guide will cover the basic ideas behind the http package, as well as examples in using, testing, and composing apps built with it.

This guide assumes you have some basic knowledge of what an interface in Go is, and some idea of how HTTP works and what it can do.

Handler

The building block of the entire http package is the http.Handler interface, which is defined as follows:

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

Once implemented the http.Handler can be passed to http.ListenAndServe, which will call the ServeHTTP method on every incoming request.

http.Request contains all relevant information about an incoming http request which is being served by your http.Handler.

The http.ResponseWriter is the interface through which you can respond to the request. It implements the io.Writer interface, so you can use methods like fmt.Fprintf to write a formatted string as the response body, or ones like io.Copy to write out the contents of a file (or any other io.Reader). The response code can be set before you begin writing data using the WriteHeader method.

Here's an example of an extremely simple http server:

package main

import (
	"fmt"
	"log"
	"net/http"
)

type helloHandler struct{}

func (h helloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "hello, you've hit %s\n", r.URL.Path)
}

func main() {
	err := http.ListenAndServe(":9999", helloHandler{})
	log.Fatal(err)
}

http.ListenAndServe serves requests using the handler, listening on the given address:port. It will block unless it encounters an error listening, in which case we log.Fatal.

Here's an example of using this handler with curl:

 ~ $ curl localhost:9999/foo/bar
 hello, you've hit /foo/bar

HandlerFunc

Often defining a full type to implement the http.Handler interface is a bit overkill, especially for extremely simple ServeHTTP functions like the one above. The http package provides a helper function, http.HandlerFunc, which wraps a function which has the signature func(w http.ResponseWriter, r *http.Request), returning an http.Handler which will call it in all cases.

The following behaves exactly like the previous example, but uses http.HandlerFunc instead of defining a new type.

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	h := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "hello, you've hit %s\n", r.URL.Path)
	})

	err := http.ListenAndServe(":9999", h)
	log.Fatal(err)
}

ServeMux

On their own, the previous examples don't seem all that useful. If we wanted to have different behavior for different endpoints we would end up with having to parse path strings as well as numerous if or switch statements. Luckily we're provided with http.ServeMux, which does all of that for us. Here's an example of it being used:

package main

import (
	"fmt"
	"log"
	"net/http"
)

func main() {
	h := http.NewServeMux()

	h.HandleFunc("/foo", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, you hit foo!")
	})

	h.HandleFunc("/bar", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "Hello, you hit bar!")
	})

	h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(404)
		fmt.Fprintln(w, "You're lost, go home")
	})

	err := http.ListenAndServe(":9999", h)
	log.Fatal(err)
}

The http.ServeMux is itself an http.Handler, so it can be passed into http.ListenAndServe. When it receives a request it will check if the request's path is prefixed by any of its known paths, choosing the longest prefix match it can find. We use the / endpoint as a catch-all to catch any requests to unknown endpoints. Here's some examples of it being used:

 ~ $ curl localhost:9999/foo
Hello, you hit foo!

 ~ $ curl localhost:9999/bar
Hello, you hit bar!

 ~ $ curl localhost:9999/baz
You're lost, go home

http.ServeMux has both Handle and HandleFunc methods. These do the same thing, except that Handle takes in an http.Handler while HandleFunc merely takes in a function, implicitly wrapping it just as http.HandlerFunc does.

Other muxes

There are numerous replacements for http.ServeMux like gorilla/mux which give you things like automatically pulling variables out of paths, easily asserting what http methods are allowed on an endpoint, and more. Most of these replacements will implement http.Handler like http.ServeMux does, and accept http.Handlers as arguments, and so are easy to use in conjunction with the rest of the things I'm going to talk about in this post.

Composability

When I say that the http package is composable I mean that it is very easy to create re-usable pieces of code and glue them together into a new working application. The http.Handler interface is the way all pieces communicate with each other. Here's an example of where we use the same http.Handler to handle multiple endpoints, each slightly differently:

package main

import (
	"fmt"
	"log"
	"net/http"
)

type numberDumper int

func (n numberDumper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Here's your number: %d\n", n)
}

func main() {
	h := http.NewServeMux()

	h.Handle("/one", numberDumper(1))
	h.Handle("/two", numberDumper(2))
	h.Handle("/three", numberDumper(3))
	h.Handle("/four", numberDumper(4))
	h.Handle("/five", numberDumper(5))

	h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(404)
		fmt.Fprintln(w, "That's not a supported number!")
	})

	err := http.ListenAndServe(":9999", h)
	log.Fatal(err)
}

numberDumper implements http.Handler, and can be passed into the http.ServeMux multiple times to serve multiple endpoints. Here's it in action:

 ~ $ curl localhost:9999/one
Here's your number: 1
 ~ $ curl localhost:9999/five
Here's your number: 5
 ~ $ curl localhost:9999/bazillion
That's not a supported number!

Testing

Testing http endpoints is extremely easy in Go, and doesn't even require you to actually listen on any ports! The httptest package provides a few handy utilities, including NewRecorder which implements http.ResponseWriter and allows you to effectively make an http request by calling ServeHTTP directly. Here's an example of a test for our previously implemented numberDumper, commented with what exactly is happening:

package main

import (
	"fmt"
	"net/http"
	"net/http/httptest"
	. "testing"
)

func TestNumberDumper(t *T) {
	// We first create the http.Handler we wish to test
	n := numberDumper(1)

	// We create an http.Request object to test with. The http.Request is
	// totally customizable in every way that a real-life http request is, so
	// even the most intricate behavior can be tested
	r, _ := http.NewRequest("GET", "/one", nil)

	// httptest.Recorder implements the http.ResponseWriter interface, and as
	// such can be passed into ServeHTTP to receive the response. It will act as
	// if all data being given to it is being sent to a real client, when in
	// reality it's being buffered for later observation
	w := httptest.NewRecorder()

	// Pass in our httptest.Recorder and http.Request to our numberDumper. At
	// this point the numberDumper will act just as if it was responding to a
	// real request
	n.ServeHTTP(w, r)

	// httptest.Recorder gives a number of fields and methods which can be used
	// to observe the response made to our request. Here we check the response
	// code
	if w.Code != 200 {
		t.Fatalf("wrong code returned: %d", w.Code)
	}

	// We can also get the full body out of the httptest.Recorder, and check
	// that its contents are what we expect
	body := w.Body.String()
	if body != fmt.Sprintf("Here's your number: 1\n") {
		t.Fatalf("wrong body returned: %s", body)
	}

}

In this way it's easy to create tests for your individual components that you are using to build your application, keeping the tests near to the functionality they're testing.

Note: if you ever do need to spin up a test server in your tests, httptest also provides a way to create a server listening on a random open port for use in tests as well.

Middleware

Serving endpoints is nice, but often there's functionality you need to run for every request before the actual endpoint's handler is run. For example, access logging. A middleware component is one which implements http.Handler, but will actually pass the request off to another http.Handler after doing some set of actions. The http.ServeMux we looked at earlier is actually an example of middleware, since it passes the request off to another http.Handler for actual processing. Here's an example of our previous example with some logging middleware:

package main

import (
	"fmt"
	"log"
	"net/http"
)

type numberDumper int

func (n numberDumper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Here's your number: %d\n", n)
}

func logger(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s requested %s", r.RemoteAddr, r.URL)
		h.ServeHTTP(w, r)
	})
}

func main() {
	h := http.NewServeMux()

	h.Handle("/one", numberDumper(1))
	h.Handle("/two", numberDumper(2))
	h.Handle("/three", numberDumper(3))
	h.Handle("/four", numberDumper(4))
	h.Handle("/five", numberDumper(5))

	h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(404)
		fmt.Fprintln(w, "That's not a supported number!")
	})

	hl := logger(h)

	err := http.ListenAndServe(":9999", hl)
	log.Fatal(err)
}

logger is a function which takes in an http.Handler called h, and returns a new http.Handler which, when called, will log the request it was called with and then pass off its arguments to h. To use it we pass in our http.ServeMux, so all incoming requests will first be handled by the logging middleware before being passed to the http.ServeMux.

Here's an example log entry which is output when the /five endpoint is hit:

2015/06/30 20:15:41 [::1]:34688 requested /five

Middleware chaining

Being able to chain middleware together is an incredibly useful ability which we get almost for free, as long as we use the signature func(http.Handler) http.Handler. A middleware component returns the same type which is passed into it, so simply passing the output of one middleware component into the other is sufficient.

However, more complex behavior with middleware can be tricky. For instance, what if you want a piece of middleware which takes in a parameter upon creation? Here's an example of just that, with a piece of middleware which will set a header and its value for all requests:

package main

import (
	"fmt"
	"log"
	"net/http"
)

type numberDumper int

func (n numberDumper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Here's your number: %d\n", n)
}

func logger(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s requested %s", r.RemoteAddr, r.URL)
		h.ServeHTTP(w, r)
	})
}

type headerSetter struct {
	key, val string
	handler  http.Handler
}

func (hs headerSetter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set(hs.key, hs.val)
	hs.handler.ServeHTTP(w, r)
}

func newHeaderSetter(key, val string) func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return headerSetter{key, val, h}
	}
}

func main() {
	h := http.NewServeMux()

	h.Handle("/one", numberDumper(1))
	h.Handle("/two", numberDumper(2))
	h.Handle("/three", numberDumper(3))
	h.Handle("/four", numberDumper(4))
	h.Handle("/five", numberDumper(5))

	h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(404)
		fmt.Fprintln(w, "That's not a supported number!")
	})

	hl := logger(h)
	hhs := newHeaderSetter("X-FOO", "BAR")(hl)

	err := http.ListenAndServe(":9999", hhs)
	log.Fatal(err)
}

And here's the curl output:

 ~ $ curl -i localhost:9999/three
 HTTP/1.1 200 OK
 X-Foo: BAR
 Date: Wed, 01 Jul 2015 00:39:48 GMT
 Content-Length: 22
 Content-Type: text/plain; charset=utf-8

 Here's your number: 3

newHeaderSetter returns a function which accepts and returns an http.Handler. Calling that returned function with an http.Handler then gets you an http.Handler which will set the header given to newHeaderSetter before continuing on to the given http.Handler.

This may seem like a strange way of organizing this; for this example the signature for newHeaderSetter could very well have looked like this:

func newHeaderSetter(key, val string, h http.Handler) http.Handler

And that implementation would have worked fine. But it would have been more difficult to compose going forward. In the next section I'll show what I mean.

Composing middleware with alice

Alice is a very simple and convenient helper for working with middleware using the function signature we've been using thusfar. Alice is used to create and use chains of middleware. Chains can even be appended to each other, giving even further flexibility. Here's our previous example with a couple more headers being set, but also using alice to manage the added complexity.

package main

import (
	"fmt"
	"log"
	"net/http"

	"github.com/justinas/alice"
)

type numberDumper int

func (n numberDumper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintf(w, "Here's your number: %d\n", n)
}

func logger(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		log.Printf("%s requested %s", r.RemoteAddr, r.URL)
		h.ServeHTTP(w, r)
	})
}

type headerSetter struct {
	key, val string
	handler  http.Handler
}

func (hs headerSetter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Header().Set(hs.key, hs.val)
	hs.handler.ServeHTTP(w, r)
}

func newHeaderSetter(key, val string) func(http.Handler) http.Handler {
	return func(h http.Handler) http.Handler {
		return headerSetter{key, val, h}
	}
}

func main() {
	h := http.NewServeMux()

	h.Handle("/one", numberDumper(1))
	h.Handle("/two", numberDumper(2))
	h.Handle("/three", numberDumper(3))
	h.Handle("/four", numberDumper(4))

	fiveHS := newHeaderSetter("X-FIVE", "the best number")
	h.Handle("/five", fiveHS(numberDumper(5)))

	h.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.WriteHeader(404)
		fmt.Fprintln(w, "That's not a supported number!")
	})

	chain := alice.New(
		newHeaderSetter("X-FOO", "BAR"),
		newHeaderSetter("X-BAZ", "BUZ"),
		logger,
	).Then(h)

	err := http.ListenAndServe(":9999", chain)
	log.Fatal(err)
}

In this example all requests will have the headers X-FOO and X-BAZ set, but the /five endpoint will also have the X-FIVE header set.

Fin

Starting with a simple idea of an interface, the http package allows us to create for ourselves an incredibly useful and flexible (yet still rather simple) ecosystem for building web apps with re-usable components, all without breaking our static checks.