title img
Golang context

The upcoming release of Go 1.7 shall move the golang.org/x/net/context package into the standard library. The move standardizes a simple interface and several simple functions that allow for straightforward concurrent processing as detailed in a 2014 Go blog post. More importantly a Context can be attached *http.Request allowing for cleaner and more concise code.

An example is a recurring pattern used in middleware or http handler processing that looks a little bit like:

func Handler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    // Some functionality that may or may not need ctx
}

More importantly it’s not clear that ctx is associated with r and that the values within ctx or functionality shall affect the request or processing. For example:

    func Handler(ctx context.Context, w http.ResponseWriter, r *http.Request) {
    e, found := entity.FromContext(ctx)
    // Processing...
}

vs

func Handler(w http.ResponseWriter, r *http.Request) {
    e, found := entity.FromContext(r.Context())
    // Processing...
}

Attaching context to a request leads to code that is easier to understand and maintain. With that aside let’s have some fun with context, requests, and concurrency.

Note: There’s plenty of code left out to make the following fully functional. The code is meant to get an idea across rather than as a functional example or even a guideline.

When processing requests getting a response to the requestor quickly should be a high, if not the highest, priority. Many times middleware processing delays a response by serially processing headers or processing authentication tokens. The typical handler flow looks like:

  1. Receive request
  2. Process cookie or auth token, create new context.WithValue
  3. Parse request, read values from context, process request, write response
  4. Write to log

The handler for step 2 may look like:

func entityHandler(w http.ResponseWriter, r *http.Request) {
    c, err := r.Cookie("auth")
    if err != nil {
        // auth not found, let's go forward.
        // Don't worry about where next came from
        // I may write a post in the future about that.
        next(w, r)
        return
    }

    // Get the entity from some lookup system. This could take a long time.
    e := getEntity(c.Value)

    // Wrap around the context and send it onwards for processing.
    next(w, r.WithContext(context.WithValue(r.Context(), entityKey, e)))
}

Pretty straightforward and typical to almost all requests (you may also have CORS, rate limiting, etc.) and almost always run serially. Step 2 may involve database operations or network activity that blocks step 3 from parsing the request prior to reading values. We can potentially reduce the time to response by continuing processing while we’re fetching the entity:

func entityHandler(w http.ResponseWriter, r *http.Request) {
    c, err := r.Cookie("auth")
    if err != nil {
        next(w, r)
        return
    }

    ed := entityData{}
    ed.w.Add(1)
    go func() {
        // Get the entity from some lookup system. This could take a long time.
        ed.e = getEntity(c.Value)
        ed.w.Done()
    }()

    // Wrap around the context and send it onwards for processing.
    next(w, r.WithContext(context.WithValue(r.Context(), entityKey, &ed)))
}

type entityData struct {
    w sync.WaitGroup
    e Entity
}

func FromContext(ctx context.Context) (Entity, bool) {
    ed, ok := ctx.Value(entityKey).(*entityData)
    if !ok {
        return Entity{}, false
    }

    // Block until processing is complete.
    ed.w.Wait()

    return ed.e, true
}

In the above we start a goroutine to get the entity while sending the request onwards for processing. If and when the entity value is needed FromContext shall block until the Entity has been retrieved; this may result in an immediate return if the Entity has already been returned. If the value is not needed then everything cleans itself up on the next garbage collector cycle.

The above is a toy example and an interesting thought. If you were to use a pattern like this then it’s worth investing in benchmarking and testing under realistic loads before you start down this route.