Context in Golang

The Context package defines an API that provides support for deadlines, cancelation signals, and request-scoped values that can be passed across API boundaries to all the goroutines involved in handling a request. This API is an essential part of any application you will write in Go. This article describes how to use the package and provides a complete working example.

Why Context:

In a Go implementation, we use goroutines to execute other functions inside the main function to run in the background simultaneously. Goroutines works out of the box to solve this issue and it is lighter than threads but when it comes to the dependency between the goroutines like any message to be passed, there are channels. The goroutines and channels are excellent concepts that not only makes one understand easily and also makes one fall in love with the language.

Have you ever had a scenario where when you started the many threads separately and happened to cancel the main process due to various reasons, but it will make it impossible to cancel all the threads that are started executing, causing the system to put an unwanted load? That is where context plays a role.

It was started initially as an experimental feature inside Google's team. You can read entirely about its implementation from Sameer Ajmani, here.

Implementation

The main implementation of Context looks like this:

// A Context carries a deadline, cancelation signal, and request-scoped values
// across API boundaries. Its methods are safe for simultaneous use by multiple
// goroutines.
type Context interface {
// Done returns a channel that is closed when this Context is canceled
// or times out.
Done() <-chan struct{}
// Err will tell why this context was canceled. A context is canceled in three scenarios.
// 1. With explicit cancellation signal
// 2. Timeout is reached
// 3. Deadline is reached
Err() error
// Deadline returns the time when this Context will be canceled if any.
Deadline() (deadline time.Time, ok bool)
// Value returns the value associated with key or nil if none.
Value(key interface{}) interface{}
}

This is how context is implemented inside the context package and it is publicly available here

Done() -> method returns a channel that acts as a signal to the running functions, when the cancellation signal gets passed, the channel gets closed then all the functions should abandon their work and return. Err() -> returns an error message to indicate why the context was canceled Deadline() -> The Deadline method allows functions to determine whether they should start work at all. It works like an expiry time Value -> It allows context to carry request-scoped data(key-value pair) to all the goroutines.

When to use it

Let's say you have started a goroutine which in turn calls several goroutines and you have decided to exit or stop the operation that you are performing, then context helps to tell all the goroutines to gracefully exit so that resource gets freed up.

Suppose, you wanted a task to be finished within a deadline or should perform within a timeout interval, else it should gracefully exit and return.

How to create a context?

The two ways to create a Context:

  • context.Background()
  • context.TODO()

both the function returns a non-nil, empty context. The only difference in using TODO instead of Background is when unclear about the implementation or if the context is not yet available.

NOTE: it is important to initialize the context in the main function. and it should be passed as a first parameter to all the child goroutines to send the signal or in communication.

Using context

The following are the methods that can be derived from an existing context

  • context.WithValue()
func WithValue(parent Context, key, val interface{}) Context

WithValue returns a copy of the parent in which the value associated with the key is valid.

the key must be comparable and should not be any built-in type to avoid collision between packages using context. This value is for a request-scoped purpose not to pass any optional parameters to the functions.

  • context.WithCancel()
func WithCancel(parent Context) (ctx Context, cancel CancelFunc)

WithCancel returns a new context derived from the parent context which has the done channel in it along with its returns cancel functions. When the cancel function is called, this derived context should terminate the goroutine and return.

This can only be called where the context was created. NOTE: Canceling this context releases resources associated with it, so code should call cancel as soon as the operations running in this Context complete. Ideally, the end of this function cancel should be called to free up the running resources.

  • context.WithDeadline()
func WithDeadline(parent Context, d time.Time) (Context, CancelFunc)

It takes the deadline time as a parameter along with the parent context. Consider it like expiry time that which the function is valid only within that time. This is very useful for cronjob time to make sure, any particular operation is not performed after that time.

  • context.WithTimeout()
func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc)

It takes a short duration of time as a parameter along with the parent context and terminates the function if it continues to run beyond the timeout period.

Example

let's implement the context concepts in a simple web server. For the sake of example, our server has only one route /users, which returns all the users that are available in our system.

func users(w http.ResponseWriter, req *http.Request) {
ctx := req.Context()
fmt.Println("server: users handler triggered")
defer fmt.Println("server: users handler ended")
select {
// implemented the wait time to simulate the database process here.
case <-time.After(10 * time.Second):
fmt.Fprintf(w, "There are no users found!
")
case <-ctx.Done():
err := ctx.Err()
fmt.Println("server:", err)
internalError := http.StatusInternalServerError
http.Error(w, err.Error(), internalError)
}
}
func main() {
http.HandleFunc("/users", users)
http.ListenAndServe(":8000", nil)
}

This program uses the default context provided by the net/HTTP package, where within our select statement we have added the wait time to compensate the background process like reading the database values or something else. Here, when the server started running, as soon as the path gets triggered /users, it waits for 10 seconds and returns the HTTP message as There are no users found! which can be seen on the browser. But the beauty of context is during the wait time if the request has been canceled, it will trigger the read-only done signal which is implemented in the next case and returns the error message to denote the reason for the context cancellation.

If we cancel the request while it is been processed, this the error message which gets printed on the terminal

server: users handler triggered
server: context canceled
server: users handler ended

The message clearly shows that the context was canceled. Similarly, the appropriate message will be thrown if it canceled due to the timeout or deadline scenarios as well.

context

Always remember the context gets flowed within all the goroutine execution just like the water. If there is any cancelation in the parent level all its child goroutines get terminated and returns. This feature is really helpful in the large scale systems where using resources for unwanted purposes results in time delay for the actual work thread.

Best practices while using context

  • context.Background() should be used only at the highest level of execution and only at once.
  • Always use context.TODO() when unclear about context or if it is yet to be implemented.
  • Context cancellation should be done before returning the function if the context was created, to make sure cleanup any running process.
  • It is not advisable to use context.WithValue. It is rarely used as the request-scoped in the API. Don't treat as the optional parameter, instead, all the important values should be passed as the function arguments.
  • Never pass nil context even if any function allows it, use context.TODO if unclear about which context to be used.
  • Every incoming request to the server should create the context.
  • Outgoing calls should accept context as its first parameter.

William Kennedy has an excellent article on this with a lot of examples on each case which can be found here



Docker up and running - notesTime Complexity