building blocks of go services

Go has many web frameworks, but none that come with the standard library. There are many to choose from but this is a difficult problem. There are advantages to choosing a very well established framework, but also disadvantages like lock-in, support and flexibility. When you do not know what you will required from a framework in 3 / 6 / 9 months, how can you make that choice?

Instead of a built in framework, go provides a number of well thought out building blocks from which you can roll your own! These building blocks mean that frameworks are unnecessary to get you off the ground and are unnecessary when you find you need to add additional pieces to your apps.

ListenAndServe

ListenAndServe listens on a TCP network address and will serve resquests on incoming connections.

It is defined as:

1
func ListenAndServe(addr string, handler Handler) error

Example usage:

1
log.Fatal(http.ListenAndServe(":8080", hndlr))

It’s the func that runs a webserver in a go application and takes and address (":8080") and a Handler

Handler

A Handler responds to an HTTP request.

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

The handler type is an interface with a single method ServeHTTP. This method is called for each request recieved by ListenAndServe and returning from this method means that the request has been handled and is finished. Inside the method, you can read from the *Request and write to the ResponseWriter

You could define a type, and implement the Hander interface and manage the request paths etc. yourself. But this quickly becomes messy…

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type endpoints struct{}

func (endpoints) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	switch r.URL.Path {
	case "/hello":
	    fmt.Fprintf(w, "hello")
	case "/world":
	    fmt.Fprintf(w, "world")
	case ...
	}
}

...

http.ListenAndServe(":8080", endpoints{})

Here, it may be a trivial example but we can see that in a real world app with complex logic required for each endpoint this will quickly get out of hand, but also that we can currently only handle GET requests. Infact, the verb is not respected at all. Adding another nested switch on the supported verbs inside each case statement will make out ServeHTTP function gross! We can also see that the endpoints struct is not used, apart from to satisfy the interface.

HandlerFunc

The HandlerFunc type is an adapter to allow the use of ordinary functions as HTTP handlers. It implements the single method defined in the Handler interface for you:

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

And calls the provided function f with w and r, all you need to do is provide a function with a method signature that matches the ServeHTTP function and the HandlerFunc interface will turn it into a Handler!

Using the same example from above you can now lose the unused endpoints struct:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func endpoints(w http.ResponseWriter, r *http.Request) {
	switch r.URL.Path {
	case "/hello":
	    fmt.Fprintf(w, "hello")
	case "/world":
	    fmt.Fprintf(w, "world")
	case ...
	}
}

...

http.ListenAndServe(":8080", http.HanderFunc(endpoints))

http.HanderFunc is not a function call, it is a type conversion of the Endpoints func to a func that implements the Handler interface. We have managed to remove the need for the endpoints struct and replaced it with a single function that is type converted into a Handler, but we still have the problems of request verbs and all the endpoints being listed in a switch in a single function.

HandleFunc

http.HandleFunc registers a function as a handler for a single endpoint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "hello")
}

func world(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "world")
}

...

http.HandleFunc("/hello", hello)
http.HandleFunc("/world", world)
http.ListenAndServe(":8080", nil)

We have removed the need for mapping the url path inside a switch statement and offloaded that responsibility to the http package. Now we need only write functions with the correct args and register them as handlers for the correct url paths.

What is the http package actually doing?

Each call to http.HandleFunc registers that function and path as a handler on the http package’s DefaultServeMux.

ServeMux

ServeMux is an http multiplexer, it pattern matches the URL of requests and the specified handlers and calls the closet matching handler. The DefaultServeMux is a ServeMux var exported from the http package. Using a ServeMux and the HandleFunc func we can far more easily and cleanly register endpoints for the web app. But we still do not have a means for handling http request methods, GET, POST, etc.

Our ServeMux only respects the url path and not the method. It would be great if we could register a handler for the url path and method.

GorillaMux

gorilla/mux is also an http multiplexer. It routes and dispatches http requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
* Requests can be matched based on URL host, path, path prefix, schemes,
  header and query values, HTTP methods or using custom matchers.
* URL hosts, paths and query values can have variables with an optional
  regular expression.
* Registered URLs can be built, or "reversed", which helps maintaining
  references to resources.
* Routes can be used as subrouters: nested routes are only tested if the
  parent route matches. This is useful to define groups of routes that
  share common conditions like a host, a path prefix or other repeated
  attributes. As a bonus, this optimizes request matching.
* It implements the http.Handler interface so it is compatible with the
  standard http.ServeMux.

Very quickly it’s obvious that this is extremely useful for all the things that might be missing from the stdlib implementaiton of a request mutliplexer. There are too many awesome features to explain here, but checkout the docs.

Wrap Up

go has an awesome stadard library, and each building block plays it’s part. Understanding each of them allows us to more easily and maintainably create a web service in go, without reaching for an off the shelf framework, becuase you probably don’t need it!