Understanding Golang's Func Type
- Introduction
- Four ways to skin a cat
- How does the adapter work?
- Why is this interesting?
- Summary/Breakdown
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 returnHello 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:
- No request parsing (serve same content regardless of request)
- Manual request parsing
- Multiplexer
- 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 bothmux.Handle
andhttp.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.
Summary/Breakdown
Here is a useful summary for you…
http.Handler
= interface
you support
http.Handler
if you have aServeHTTP(w http.ResponseWriter, r *http.Request)
method available.
http.Handle("/", <give me something that supports the http.Handler interface>)
e.g. an object with a
ServeHTTP
method.
http.HandleFunc("/", <give me any function with the same signature as ServeHTTP >)
e.g. a function that accepts the arguments
(w http.ResponseWriter, r *http.Request)
.
http.HandlerFunc
= func type used internally byhttp.HandleFunc
e.g. it adapts the given function to the
http.HandlerFunc
type, which has an associatedServeHTTP
method (that is able to call your original incompatible function).
But before we wrap up... time (once again) for some self-promotion 🙊