Context in go 101

msingh
5 min readJul 11, 2020

There is tonne of information available on Go Concurrency and context usage like the context package, this blog and this but it can be a bit overwhelming. This article attempts to explain Context via a simple but perhaps the most common use case you would find in Microservices architecture.

Pre-Requisite

Article assumes that Reader is familiar with Goroutines, channels and HTTP

Why Context?

The primary idea behind context is the ability to cancel unneeded work.

  • A calls B
  • B starts to do work
  • While B is working, A no longer needs the result either because B is slow, or A’s caller no longer needs the result (say because of a user action or expiry of some deadline)

How does A walk away and let B know to stop? B needs the advice to stop so that it gets a chance to cleanup any resources in use.

That’s where context comes in. In the rest of the article we will see an example, which I think is the most common usage of Context

HTTP GET call to a Slow Server

The picture below depicts a simple REST client calling a GET on HTTP Server that takes time to respond.

Waiting for a Slow Server

Here is the sample code

  • A Sample slow server is started . The handler sleeps for 10 second before responding to any request.

TIP: Uses httptest package, an excellent tool for writing HTTP Tests)

  • Client makes a request and then waits for 10 seconds before it gets the response from Server

What if the Client wants to bail out if the response doesn’t return within 5 seconds?

In Microservices architecture, cutting the chord on a slow response is a common practice (fail-fast). It also reduces pressure on the overloaded server which may be the reason behind the higher response latency. Ideas like Circuit Breaker are built around this concept.

A Timeout handling Pattern with Context

For the impatient, here is the full code. In the sections below we will go through it step by step

Step 1

For better code organization we define a type called ResponseHandler . This is a function that can process the result of http request. Implementation of this handler can hence contain any custom logic as required

Here is the func type and our super simple handler that just prints response to stdout

//Func Type to handle response
type ResponseHandler func(resp *http.Response, err error) error
//Implementation of Response Handler
func stdOutHandler(resp * http.Response, err error) error {
if err != nil {
return err
}
defer resp.Body.Close()
//Handle the response
//In this case we just print the body
body, _: = ioutil.ReadAll(resp.Body)
fmt.Printf("Body from response %s\n", string(body))
return nil
}

Step 2

  • Create Request with a Timeout context. This is the key
ctx, cancel := context.WithTimeout(context.Background(), timeout)request, _ := http.NewRequestWithContext(ctx, "GET", getURL, nil)

Things to note here

  • Context is a Tree i.e a Parent with many descendants
  • Background is the root of any Context tree (Represented by the code context.Background()) .

From Go Doc : Background returns an empty Context. It is never canceled, has no deadline, and has no values. Background is typically used in main, init, and tests, and as the top-level Context for incoming requests.

  • context.WithTimeout(context.Background(), timeout) creates a copy of context with a deadline set to current time + specified timeout

Step 3

  • Do the request with context in a Goroutine.
fResult: = make(chan error, 0)
go func() {
fResult < -respHandler(http.DefaultClient.Do(request))
}()

Why separate Goroutine? Well, what we want is to make this call and then move on to monitor the “status” of the call via the Context mechanism. So the request shouldn’t be blocking us to proceed further and do the Select

Step 4

*️⃣ The Select

  • Recall that our objective is to process the response if it happens before the specified timeout, else give up on the request
select {
case <-ctx.Done():
< -fResult //let the go routine end too
return ctx.Err()
case err:= < -fResult:
return err //any other errors in response
}
  • When we create a derived Context, in this case with timeout, the returned context has a Done channel. Done channel is closed when the deadline expires and hence works as a perfect broadcast mechanism

So in the above code snippet, what we are doing is waiting for one of the two things to happen

  1. The deadline expires (Done channel is closed)

OR

  1. The GET is successful and the ResponseHandler processes the response. Any errors in processing are returned in the error channel.

cancel function : Creating the derived context with timeout also returns a CancelFunc. It’s important that the function be called when the operation running in the context completes, so as to release any resources associated with it and prevent leaks. In the code above we do a defer cancel() immediately after creating the context

Example Run

Running the gist, would report the following error (your port may differ since the httptest server picks a random port)

Error in GET request to server http://127.0.0.1:60171, error = context deadline exceeded

This is because in the test code we create a context with timeout of 6 seconds whereas the Slow Server takes 10 seconds to respond

Context is Propagated

We saw that handling timeout on the HTTP Client allowed the caller to walk way from a slow response, but what about the Server? Wouldn’t it be nice if the Server could also know that the Client no longer cares about the result and stops processing it.

Turns out that golang Context is indeed transitive and propagated to the Server.

Let’s modify the server code to also “listen” to context signals -

  • The line ctx := r.Context() retrieves the context associated with the request
  • We now use the same methodology as used in case of request — start a Goroutine which does the job (in this case just sleeps for 12 seconds and then writes “Hello There” ).
  • After the Goroutine is spawned, the handler starts to wait on either a message on Done channel or from the channel fed by Goroutine
  • In our current case, since the timeout on the request is 6 second and the job takes 12 seconds, what we would see is the following error on the server side — Error context canceled when the context is canceled by the client.

A high level sequence diagram is shown below -

Context Propagation

Next

This is just the tip of iceberg but hopefully provides enough “context” to try more complicated scenarios of Context usage.

--

--