diff options
author | Brian Picciano <mediocregopher@gmail.com> | 2021-01-21 17:22:53 -0700 |
---|---|---|
committer | Brian Picciano <mediocregopher@gmail.com> | 2021-01-21 17:22:53 -0700 |
commit | bcf9b230be6d74c71567fd0771b31d47d8dd39c7 (patch) | |
tree | 2d0fc16142d55bbd5876ac6b8174c2857883b40e /_posts/2015-07-15-go-http.md | |
parent | d57fd70640948cf20eeb41b56e8d4e23e616cec0 (diff) |
build the blog with nix
Diffstat (limited to '_posts/2015-07-15-go-http.md')
-rw-r--r-- | _posts/2015-07-15-go-http.md | 547 |
1 files changed, 0 insertions, 547 deletions
diff --git a/_posts/2015-07-15-go-http.md b/_posts/2015-07-15-go-http.md deleted file mode 100644 index 7da7d6b..0000000 --- a/_posts/2015-07-15-go-http.md +++ /dev/null @@ -1,547 +0,0 @@ ---- -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. |