Understanding Golang's func type

Datetime:2016-08-23 05:16:36          Topic: Golang  DataBase           Share

Introduction

Here is some code that demonstrates the typical 'hello world' for a Go based web server:

package main

import (
  "fmt"
  "net/http"
)

func handler(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "Hello %s", r.URL.Path[1:])
}

func main() {
  http.HandleFunc("/World", handler)
  http.ListenAndServe(":8080", nil)
}

Note: http://localhost:8080/World will return Hello World

For most people, setting up a web server to handle incoming HTTP requests is considered a quick and simple introduction to the Go programming language , and looking at the above code it's easy to see why that would be the case. But further investigation can yield some nice learnings about Go's built-in func type and how it is used as an adapter layer.

In this blog post I will demonstrate a few different ways of creating a web server and then I'll clarify how some of the functionality (specifically http.HandleFunc ) works. What initially drove me to look into this was my curiosity as to why I would always insert nil to the http.ListenAndServe function by default when setting up a basic web server (see above code example).

It was never really that clear to me and so it's just something I 'cargo cult'ed and subsequently replicated every single time I needed a web server. I realised I needed to know what its purpose was in order to feel like I wasn't going through the motions unnecessarily or missing out on additional functionality (which it turns out I was).

Four ways to skin a cat

There are currently four ways, that I know of, to create a web server with Go (well, actually only three - the first two examples are effectively the same - but we add a little more code to demonstrate different ways incoming requests can be handled).

Each of the variations ultimately revolve around what we send to http.ListenAndServe as its second argument (and this 'thing' we send also ultimately should have a ServeHTTP method; we'll see shortly how this is achieved in different ways).

So here are each of the variations:

  1. No request parsing (serve same content regardless of request)
  2. Manual request parsing
  3. Multiplexer
  4. Global multiplexer

No request parsing

The most basic implementation (and by basic I don't mean 'simplest', more... 'raw') is demonstrated in the below code sample, which calls ListenAndServe and passes in db as its second argument.

Note: although I wrote this blog post back in October 2015, I've rewritten the below examples based off inspiration from "The Go Programming" book I've been reading recently

This first section will give us enough background and grounding to build upon in the latter sections:

package main

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

type pounds float32

func (p pounds) String() string {
  return fmt.Sprintf("£%.2f", p)
}

type database map[string]pounds

func (d database) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  for item, price := range d {
    fmt.Fprintf(w, "%s: %s\n", item, price)
  }
}

func main() {
  db := database{
    "foo": 1,
    "bar": 2,
  }

  log.Fatal(http.ListenAndServe("localhost:8000", db))
}

We can see from the above code sample that db is an instance of our custom database type, which states it should be a map data structure consisting of strings for keys and pounds for values.

We can also see that pounds is itself a type of float32 and has a custom String method attached, allowing us to modify its output when converted into a string value. Similarly the database type has a method attached, but this time it is a ServeHTTP method.

The ServeHTTP is required in order to satisfy the ListenAndServe method signature, which states the second argument should be a type of Handler :

func ListenAndServe(addr string, handler Handler) error

Documentation: godoc net/http ListenAndServe | less

If we look at the source code for the Handler type (below) we can clearly see it requires a ServeHTTP method to be available (hence why our database type associates its own ServeHTTP method):

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

Documentation: godoc net/http Handler | less

The above sample web server code will always serve the same response regardless of the URL that was specified. So for example...

  • http://localhost:8000/
  • http://localhost:8000/abc
  • http://localhost:8000/xyz

...will all serve back the response:

foo: £1.00
bar: £2.00

Manual request parsing

OK, so now we've got the above example written. Let's enhance it by allowing our application to handle different routes as apposed to serving the same content all the time. To do this we'll modify our ServeHTTP method to interrogate the incoming request object and parse out the URL, as demonstrated in the below code sample:

package main

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

type pounds float32

func (p pounds) String() string {
  return fmt.Sprintf("£%.2f", p)
}

type database map[string]pounds

func (d database) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  switch r.URL.Path {
  case "/foo":
    fmt.Fprintf(w, "foo: %s\n", d["foo"])
  case "/bar":
    fmt.Fprintf(w, "bar: %s\n", d["bar"])
  default:
    w.WriteHeader(http.StatusNotFound)
    fmt.Fprintf(w, "No page found for: %s\n", r.URL)
  }
}

func main() {
  db := database{
    "foo": 1,
    "bar": 2,
  }

  log.Fatal(http.ListenAndServe("localhost:8000", db))
}

Nothing else to say about this, other than we've implemented what we set out to do by utilising a simple switch statement that checks for known paths and writes to the http.ResponseWriter a different response depending on the request. If we can't match the URL then we'll instead send a 404 status code ( StatusNotFound ) followed by a message to notify the user we couldn't identify their request.

Documentation: godoc -src net/http WriteHeader | less

Multiplexer

So writing the above example demonstrates a bit of a code smell. We could extract each case's block into separate functions but it's still an ever growing switch statement. We're also confined to using objects that implement the required interface (e.g. if you don't provide an object that has a ServeHTTP method then you're not going to have much success).

Instead it would be nice if you could just pick an arbitrary function and allow it to be used as a handler. That's exactly what ServeMux provides to us via its HandleFunc function (which is really just a convenience method on top of http.HandlerFunc ).

Documentation: godoc net/http ServeMux | less

The following code sample demonstrates this in action, by removing the ServeHTTP method from the database type and instead implementing individual methods for our defined routes to call.

package main

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

type pounds float32

func (p pounds) String() string {
  return fmt.Sprintf("£%.2f", p)
}

type database map[string]pounds

func (d database) foo(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "foo: %s\n", d["foo"])
}

func (d database) bar(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "bar: %s\n", d["bar"])
}

func (d database) baz(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "baz: %s\n", d["baz"])
}

func main() {
  db := database{
    "foo": 1,
    "bar": 2,
    "baz": 3,
  }

  mux := http.NewServeMux()

  mux.Handle("/foo", http.HandlerFunc(db.foo))
  mux.Handle("/bar", http.HandlerFunc(db.bar))

  // Convenience method for longer form mux.Handle
  mux.HandleFunc("/baz", db.baz)

  log.Fatal(http.ListenAndServe("localhost:8000", mux))
}

As we can see, we create a new ServeMux instance using http.NewServeMux and then register our database methods as handlers for each of the route's we want to match them against. The ServeMux instance is a multiplexer, meaning we can pass it as the second argument to http.ListenAndServe .

Note: you can also see we demonstrate the shorthand mux.HandleFunc which is really a convenience method over both mux.Handle and http.HandlerFunc

So how does http.HandlerFunc and mux.HandleFunc allow us to use an arbitrary function (as none of those database functions have access to a ServeHTTP function as required by ListenAndServe )? We'll come back to the answer in a little bit. Let's quickly review the last variation of how to run a web server first...

Global multiplexer

Typically you'll have your code split up into separate packages. So in order to setup your routing handlers, you would need to pass around your ServeMux instance to each of these packages. Instead, you can just utilise Go's global DefaultServeMux . To do that you pass nil as the second argument to http.ListenAndServe .

Documentation: godoc -src net/http DefaultServeMux | less

The following code sample demonstrates this:

package main

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

type pounds float32

func (p pounds) String() string {
  return fmt.Sprintf("£%.2f", p)
}

type database map[string]pounds

func (d database) foo(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "foo: %s\n", d["foo"])
}

func (d database) bar(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "bar: %s\n", d["bar"])
}

func (d database) baz(w http.ResponseWriter, r *http.Request) {
  fmt.Fprintf(w, "baz: %s\n", d["baz"])
}

func main() {
  db := database{
    "foo": 1,
    "bar": 2,
    "baz": 3,
  }

  http.HandleFunc("/foo", db.foo)
  http.HandleFunc("/bar", db.bar)
  http.HandleFunc("/baz", db.baz)

  log.Fatal(http.ListenAndServe("localhost:8000", nil))
}

Again, we have a convenience method HandleFunc which allows an arbitrary function to be adapted so it fits the interface requirements that ListenAndServe 's second argument enforces.

How does the adapter work?

The 'adapter' here being the http.HandleFunc function. How does it take an arbitrary function and enable it to support the relevant interface so it can be passed to ListenAndServe ?

The way http.HandleFunc solves this requirement is by internally calling its other function http.Handle , and passing it the required type (i.e. it passes a type that satisfies the interface requirement that the Handle function has).

OK, let's look back at the two functions and their respective signatures to refresh our memory as to what's required:

  • func Handle(pattern string, handler Handler)
  • func HandleFunc(pattern string, handler func(ResponseWriter, *Request))

We can see the Handle signature requires a type that satisfies the Handler interface (which is defined as follows):

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

In other words, as long as you pass in a type that has a ServeHTTP method then the Handle function will be happy. So HandleFunc facilitates this requirement by taking your user defined function and converting it into a type that happens to have ServeHTTP available.

So how does it do that conversion? Firstly it defines a func type called http.HandlerFunc , like so:

type HandlerFunc func(ResponseWriter, *Request)

This says that for a function to match this type it should have the same signature (e.g. ResponseWriter, *Request ).

Inside the HandleFunc function you'll see it actually calls this func type and passes it your user defined function. This will look something like the following in the Go implementation source code:

func (mux *ServeMux) HandleFunc(pattern string, handler func(ResponseWriter, *Request)) {
  mux.Handle(pattern, HandlerFunc(handler))
}

Notice the call of HandlerFunc(handler) (where handler is your user defined function you passed into HandleFunc from your application code). This is the conversion of your function into the HandlerFunc type. You're now effectively passing a HandlerFunc into the internal function mux.Handle .

So how does that help us? How does passing in a function that looks like a HandlerFunc type into mux.Handle help us solve the problem that we're still passing in a function that has no ServeHTTP method available (and so should fail the interface requirement that mux.Handle has)?

Well, once you convert your user defined function into a HandlerFunc you'll find it now does have a ServeHTTP method available. If we look at the Go source code, just after the definition of the HandlerFunc func type, you'll also find the following snippet of code:

func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
  f(w, r)
}

This associates the required ServeHTTP function with the HandlerFunc type. So when you convert your function to a HandlerFunc it will indeed gain access to a ServeHTTP function!

Also remember that when you associate a method with a type/object the receiver is also available to you. So in this case we can see the f is actually your user defined function you passed in to be converted. So when you convert that user defined function into a HandlerFunc you get the ServeHTTP method which internally is calling your original user defined function.

Let's now take a quick look at that mux.Handle function to see what it expects:

func (mux *ServeMux) Handle(pattern string, handler Handler) {
  ...
}

As we can see it expects a type of Handler to be provided. What is Handler ? Well remember from earlier this is an interface which states there should be a ServeHTTP function available:

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

We know now that we've utilised Go's func type to adapt/transform our incoming function into a type that has the required method ServeHTTP associated with it, thus allowing it to pass the Handler interface requirement.

Why is this interesting?

Really understanding what initially looked to be a simple web server abstraction ended up being a complex mix of types and interfaces that work together to allow seemingly incompatible types to be adapted to fit. Demonstrating how flexible and dynamic your code can be when working in an idiomatic way with the Go principles.

I now have a much better appreciation of why lots of long time Gophers will routinely recommend sifting through the official Go source code, as it can indeed be quite enlightening.

Links





About List