GOLANG PACKAGE
Understanding the context
package
Communication between goroutines can be troublesome at times using channels as the only communication medium. In this article, we are going to look at the context
package to send cancellation signals between API boundaries.

In a data and network intensive Go program, you probably going to use concurrency patterns where multiple goroutines, running concurrently or in parallel, are handling various tasks of the execution.
As we know, channels are the safest way to communicate between different goroutines. These channels carry data or messages between different goroutines. Channels can also be used to send cancellation signals across goroutines so that goroutines can stop their execution.

In the above program, we are creating a printNumbers
goroutine and then sleeping the main
goroutine for a second. Until then, the printNumbers
goroutine is running an infinite for
loop in the background.
By the time main
goroutine is awake again, printNumbers
is still working in the background or at least holding the system resources that could be useful for other things in the program. We can verify this from the output. By the time main
goroutine returns, we still have 2
active goroutines.
This is a very unusual example but it has a deeper meaning. What we want is that once the main
goroutine is awoken, we want to stop the printNumbers
goroutine, releasing the system resources and unnecessary CPU overhead. This can be done using a channel to send a stop signal.

In the above example, we have created a signal
channel to communicate between main
goroutine and printNumbers
goroutine. Once the main
goroutine awakes, it blocks again by sending a value to the signal
channel.
Due to the select
statement in the printNumbers
goroutine, it will always execute default
case since signal
won’t have a value to read from, at least for 1 second. Once the signal
has a value, return
statement will be executed which will kill the goroutine and so the for
loop.
When the main
goroutine returns, we have only 1
active goroutine. This way, we can clean up any resources hogged by the printNumbers
goroutine. This way of communication between two goroutines can be used for simple operations like the above.
However, when there is a tight relationship between running goroutines and one goroutine can start other goroutines, then it becomes tremulously difficult to use channels to send cancellation signals.
It could be the case that when one goroutine can start other goroutines, and those goroutines start other goroutines and so on, then the first goroutines should be able to send cancellation signals to all spawned goroutines.
For example, if the printNumbers
goroutine starts another goroutine, the main
goroutine won’t have any idea about it. So it becomes the responsibility of the PrintNumbers
goroutine to send a cancellation signal to it. This gets more complicated when that goroutine spawns other goroutines.
This is where context
package comes in. The sole purpose of the context
package is to carry out the cancellation signal across goroutines no matter how they were spawned, context
got them covered.
An object of interface type context.Context
is used to carry deadlines, cancelation signals, and values between concurrently running processes. This object provides a Done
signal channel that closes when we call the cancel function received during the creation of this object. This channel is be retrieved using the Done()
method of the Context
object.
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
In nutshell, we create a Context
object in one goroutine and pass it to the other goroutine. The other goroutine retrieves the signal channel from the context using Done()
and starts its work. Once the Done
channel closes, the goroutine stops whatever it was doing and immediately returns.
A Context
can be time-dependent. It can close the signal channel after some definite time. We can specify a deadline or a timeout after which the Context
object should close the signal channel.
The best thing about Context
is that it can be derived from the other context object. When you create a Context
from another Context
object, these contexts form a symbiotic relationship. If the parent Context
closes its Done
channel, the child’s Done
channel is automatically closed and so do the Done
channels of any Context
objects derived from the child.
A Context
object can also hold a single value. We can derive a child Context
from a parent Context
with a value. Using the Value(key)
method on the child context, this value can be retrieved.
Now we can presume that Context
plays a huge role in making concurrent programs secure and efficient. Let’s dive into some real-life use cases of the Context
and see how it behaves in certain conditions.
context.Background()
An empty context is a Context
that has no value, no deadline and it’s never canceled. The context.Background()
function returns a default empty Context
. This Context
is generally used to derive other context objects since it never cancels. It can also be used in test cases or merely to pass a context object to an API where custom context is not important.
Since Context
is an interface, its value can also be nil
. But it is not recommended to pass a nil
context. In such cases, context.Background()
is a good option. You can also use context.TODO()
which also returns an empty Context
that can be used as a placeholder when the actual implementation of the Context
is not yet decided.
context.WithCancel
The context.WithCancel
function returns a derived Context
object and a CancelFunction
. The Done
channel of this Context
is closed when the cancel function is called or when the parent’s Done
channel is closed, whichever happens first.
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

In the example above, we have created a channel c
that transports the squares of integers. The square
goroutine runs an endless for
loop and sends the square of the next integer sequentially.
Inside the main
goroutine, we are reading square of the integers using for
loop for 5
iterations. After that, we are basically doing other jobs and square
goroutine is just waiting idle, doing nothing.
By the time main
goroutine returns, we are still having 2
goroutine. To solve this issue, we could close the channel c
and use val, ok := <- c
syntax inside square
goroutine to break the for
loop.
But at times, we do not have direct control over the channel (such as the receive-only channel) or the mechanism could be complex to handle. This where Context
is the best idea.

In the example above, we have derived a Context
from context.Background
using context.WithCancel
function. This returns a copy of the parent context context.Background
with a new Done
channel and a cancel function to cancel the context anytime we want.
This context is passed to the square
goroutine. Inside square
goroutine, we are using select
statement inside the for
loop. This select
block contains 2
cases. The first case executes if the Done
channel of the context has a value to read from which would be true
if the Done
channel is closed. But until the cancel function is called, this case won’t execute. The second case writes square of the next integer to the channel c
and blocks.
After 5
iterations in the main
goroutine, we are calling the cancel function. This will close the Done
channel of the ctx
and hence whenever the square
goroutine is scheduled, the first case inside select will be executed which will terminate the square
goroutine.
Normally, people prefer to use defer cancel()
to cancel a context when the goroutine returns. However, canceling a Context
releases the resources associated with it. Hence it is recommended to call cancel
as soon as context is no longer needed and when the child goroutines should stop their jobs.
A context can be passed to multiple goroutines. Also, calling cancel
after the first cancel
call does absolutely nothing. In the above example, cancel
did not close the Done
channel of the parent, just so you know.
A context should be passed to functions or goroutines as an argument only, preferably with the parameter name ctx
. It is not recommended to store context inside a struct to pass around.
context.WithDeadline
Apart from a standard cancel function, if we want a Context
to cancel at a certain given time, then context.WithDeadline
is used.
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)
Similar to context.WithCancel
, this function also returns a cancel function which can be invoked manually to cancel the context. The d
argument is a specific time in the future at which the Done
channel of this context should close automatically.
If the parent
context has a deadline and it is earlier than the specified d
then the new context is semantically equivalent to the parent.
In this case, the Context
object will cancel if the parent’s Done
channel is closed, or when the cancel function of this context is called, or when the deadline d
expires, whichever happens first.
This type of Context
is useful to perform time-sensitive operations where a group of concurrently running processes must end at the same time.

In the above example, the deadline of the context ctx
has been set to a time, 3 seconds in the future. We have used Time.Add()
method to generate a Time
by adding 3
seconds to the current time.
We have passed this context to the three worker
goroutines that execute a certain job after n
seconds. Inside this worker
goroutine, using the select
statement, we are waiting for n
seconds to pass and then execute a task.
However, if the context ctx
expires earlier, then we should ideally kill the worker
goroutines by returning from the function. This will release any resources associated with the goroutine and the job it is awaiting to execute won’t execute in the future accidentally (if it gets scheduled).
The order of case blocks in the above example doesn’t matter since it is highly unlikely for both the channels to have the data at the same time. If both channels have the data (when both channels are unblocking), then select
would pick a random channel to read data from. This means the worker
goroutine has equal chance of executing the job or skipping it.
context.WithTimeout
The context.WithTimeout
function returns a context with the deadline set to some duration in the future from the current time.
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)
The function syntax is similar to the context.WithDeadline
except the second argument which is time.Duration
. Internally, context.WithTimeout
calls the context.WithDeadline
function and generates the deadline by adding timeout
to the current time.
func WithTimeout(p Context, t time.Duration) (Context, CancelFunc) {
return WithDeadline(p, time.Now().Add(t))
}
In the previous example, since we manually created a deadline by adding 3
seconds to the current time, context.WithTimeout
could have been the ideal approach like deadline := context.WithTimeout(3 * time.Second)
.
This kind of Context
can be used when an operation should be timed out after some duration such as an HTTP request. However, Go provides a built-in mechanism to deal with timeout-related issues in most of the APIs.
The context
package also provides WithValue
function to derive a Context
from a parent context with a value. However, there are certain restrictions on what key
should be used to put the value in the context, read more here.

