Lost Among Notes

<< Newer Finding Inspiration in Typography (looking from tech)
Older >> A Simple Reusable Scheduler in Go

HTTP status in Go. Or, composition rules

If C++ and Java are about type hierarchies and the taxonomy of types, Go is about composition.

from Rob Pike in Less is exponentially more.

We can see Go’s pervasive philosophy of composition at play in the net/http package.

Say you’ve built a server in Go, and you would like to know when handlers return a 200-range code or a 500-range code. For example, you may want to capture metrics for your Prometheus. Or, you may want to alert your crash-reporting system on every 500.

However, the canonical Go signature for HTTP handlers doesn’t seem to help:

func myHandler(http.ResponseWriter, *http.Request)

The function returns nothing. How can you capture the HTTP status after execution?

If you do a search on the web, you’ll very easily find the following approach: http.ResponseWriter is an interface, so you can create an implementation of your own, with a field to hold the returned HTTP status.

Say

type myResponse {
    http.ResponseWriter
    Status int
}

Since we took advantage of embedding, we get the three interface methods for free by promotion. All we need to do is re-implement WriteHeader with the extra logic to capture status:

func (m *myResponse) WriteHeader(statusCode int) {
    m.Status = statusCode
    m.ResponseWriter.WriteHeader(statusCode)
}

After a myResponse has been written to, you can check its Status. But how are you supposed to ensure that the existing handlers in your codebase use myResponse?

Again, because http.ResponseWriter is an interface, and a small one, it is easy to write a middleware to wrap existing handlers and inject the custom response writer. The convenient http.HandlerFunc type helps us clarify signatures.1

func wrapHandler(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        mw := &myResponse{w, 0}
        next(mw, r)
        log.Println("status was", mw.Status)
    }
}

If you’ve had the foresight to package your handlers across your codebase so that the URL and the handler function are both available, it will be easy to wrap all your endpoints with the wrapHandler middleware.

type EndPoint struct {
    Pattern string
    Handler http.HandlerFunc
}

func main() {
    
    myMux := http.NewServeMux()
    
    for _, ep := range myFooService.EndPoints {
        myMux.HandleFunc(ep.Pattern, wrapHandler(ep.Handler))
    }
    

Note that:

  • our custom ResponseWriter is transparent to the rest of the codebase. Your colleagues do not need to make any changes to the HTTP handlers they wrote
  • our middleware is also transparent to the existing HTTP handlers. We just added it like a LEGO piece. Someone else could write an additional middleware to add extra behavior, and wrap ours

Go makes this type of composition very easy. But it’s not just that the language offers interfaces and first-class functions. It’s a whole philosophy, one informed by the Unix design of pipes, and its continuation in Plan 9.2

In a previous job, I was creating applications for the Catastrophe Model sector (an engineering branch of the insurance industry.) I had started a Prometheus service to capture internal usage metrics.

I only had a few endpoints I wanted tracked, each of which generated a different kind of report. All reports were highly dependent on country, model (commercial catastrophe model used), and peril (earthquake, hurricane, flood, …)

My approach then was to reify the bits I was interested in tracking, which were more than just an HTTP code:

// AppCoords represents the data we care about w.r.t. metrics
type AppCoords struct {
    ReportKind string
    Country    string
    Peril      string
    Model      string
}

// AppHandler represents a web app handler, but returning error
// and AppCoords,
// i.e. it requires transformation to serve as a http.Handler
type AppHandler interface {
    ServeApp(http.Re[], *http.Request) (AppCoords, int, error)
}

func WrapAppHandler(counter *prometheus.CounterVec,
    ah AppHandler) http.Handler {

    return http.HandlerFunc(func(w http.Re[], r *http.Request) {

        coords, statusCode, err := ah.ServeApp(w, r)
        // my prometheus code followed here

This is a very different approach, and today I might have done things differently. The point is that hooking metrics up and writing middlewares is easy to do.

Because the interface abstraction in Go is so pervasive and flexible, you can think of myriad ways to mix and match, reuse, and build new pieces that fit together.

But in order for this to work well, interfaces should be small. An interface with 23 methods is hardly an invitation for reuse and composition.

Again, Rob Pike says it best, in Go Proverbs (video)

The bigger the interface, the weaker the abstraction.


  1. the HandlerFunc type is a function type. It also has a ServeHTTP method that allows it to satisfy the Handler interface. Yes, a function can have methods in Go! ↩︎

  2. It’s no coincidence, given that Rob Pike and Ken Thompson, as well as other Go core team members, worked on both Unix and Plan 9. ↩︎