mediocre-blog/_posts/2015-07-15-go-http.md

548 lines
15 KiB
Markdown
Raw Permalink Normal View History

---
title: Go's http package by example
description: >-
The basics of using, testing, and composing apps built using go's net/http
package.
---
Go's [http](http://golang.org/pkg/net/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:
```go
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:
```go
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.
```go
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:
```go
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](http://www.gorillatoolkit.org/pkg/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.Handler`s 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:
```go
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:
```go
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:
```go
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:
```go
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](https://github.com/justinas/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.
```go
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.