Basic Contexts in Golang

Learn via video courses
Topics Covered

Overview

In Go, the context package allows you to transport request-scoped data, cancellation signals, and deadlines across API boundaries. It is intended to function in conjunction with the request context integrated into the Go standard library. It enables the transmission of cancellation signals and request-scoped values across API boundaries, simplifying the design and justification of concurrent systems. The context package can also be used in server-side and client-side applications to timeout or abort long-running actions. It is a strong tool for managing the flow of execution in a concurrent system, and it is heavily utilized in Go's standard library as well as many popular Go packages.

Introduction to Context in Golang

Context is a Golang standard package that allows you to easily pass request-scoped variables, cancellation signals, and deadlines to all the goroutines involved in handling a request across API boundaries. The official documentation of Go defines the context as "Package context defines the Context type, which carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes."

Context is one of the most useful packages in Go. Let's see why to use context in golang.

  • It makes applying deadlines and cancellations across your processes or API easier.
  • Goroutine safe, i.e., you can run the same context on separate goroutines without leaks.
  • The go context package can be helpful while interacting with APIs and slow processes, specifically in production-grade systems that handle web requests, where you might want to notify all the goroutines to stop work and return.
  • It prepares your code for scaling; for example, the context package will make your code clean and easy to manipulate in the future by chaining all your process in a child-parent relationship, you can join any process together.

Syntax

The primary entity in the context package in Go is Context itself, which is an interface. It has only four entity which is mentioned below:

where,

  • Deadline : Returns the time when the context should be canceled, as well as a Boolean that is false if no deadline exists.
  • Done : Returns an empty struct receive-only channel that notifies when the context should be canceled.
  • Err : If the done channel is open, it returns nil; otherwise, it returns the cause of the context cancellation.
  • Value : Returns a value associated with a key in the current context or nil if the key has no value.

Example

Handling HTTP requests is a common use case for context. Here is an example of an HTTP server with a handler exampleHandler(). We show context by handling request cancellation. Each request, by default, has an associated context that may be accessed via the request. Context() method. We use the context's Done channel to keep track of any cancellation signals.

You can now run the server locally and connect to localhost:5000/exampleHandler. If you cancel the request before it is served and it is inside the first 10 seconds, the request handler will graciously process the cancellation and print the server error.

Creating a Context

Let's write a program that includes a function that takes the context as a parameter. You will also call that function using an empty context created with the context.TODO, as well as context.Background functions.

First let's create a main.go file. In the main.go file create a doSomething function that takes context.Context as an argument. Then you'll include a main function that generates a context and then calls doSomething function with that context.

In the above program, we utilized the context.TODO in the main function. It is one of two methods for creating an empty context. You can use this as a placeholder when you're unsure which context to use. The output of the above program will look like this.

Output:

Now let's change our main.go file and try to create an empty context using the context.Background() function.

The context.Background function also creates an empty context, just like context.TODO, but it's intended to be used in situations where you want to start a known context. Generally, both functions are identical, as both return an empty context that may be used as a context.Context. The major difference is in the way you let other developers know what your intentions are. Suppose you need help deciding which one to use, context.Background is a good default option.

The output of this program is similar to the above code.

Output:

Using Data within a Context

Accessing data stored inside a context is one advantage of using context.Context in a program. Each program layer can provide more information about what's happening by adding data to a context and forwarding the context from function to function. For instance, the first function might update the context with a username. The following function might add the file path to the content the user is attempting to access. A third function might then read the file from the system's disc and log whether it was loaded successfully or not as well as which user had attempted to load it.

Use the context.WithValue function in the context package to add a new value to a context. The function takes three parameters: the parent context.Context, the key, and the value. The parent context is the context to which the value should be added while retaining all other information about the parent context. The value is then retrieved from the context using the key. The key and value can be any data type, however in this lesson, string keys and values will be used. The context.WithValue function will then return a new context.Context value that includes the value.

Now, go to your main.go file and edit it to add a context value using context.WithValue. Then, modify the doSomething function and use fmt.Printf to print that value to the output.

In this code, you're returning the new context to the cntx variable, which holds the parent context. This is a frequent pattern to utilize if you don't need to refer to a specific parent context. If you do require access to the parent context, you can store it in a separate variable.

Output:

Ending a Context

The context.Context package also offers the ability to notify functions that the context has ended and should be considered finished. By sending this signal, functions know to stop any work related to the context that they may still be performing. This context feature improves program efficiency by preventing the completion of functions that would otherwise be unnecessary, allowing the processing time to be used for other tasks.

Generally, there are four different ways to end a context and these are as follows:

  1. Determining if a Context is Done
  2. Canceling a Context
  3. Giving a Context a Deadline
  4. Giving a Context a Time Limit

Determining if a Context is Done

Regardless of why a context ends, you can check if it has ended by using the Done method provided by the context.Context type. This method returns a channel that will be closed once the context is done, and any functions that are monitoring it will know that they should stop any processing related to that context as it is considered complete.

Canceling a Context

Canceling a context is a straightforward and easy way to end it. Using context.WithCancel, you can associate a cancel function with a context, which can be used to end the context. The context.WithCancel function takes a parent context as input and returns a new context and a cancel function. The cancel function only ends the returned context and any contexts that use it as a parent, but it doesn't stop the parent context from being canceled.

Giving a Context a Deadline

The context.WithDeadline feature of the context package in Go provides a way to set a deadline for a context to end. The deadline can be set by giving the parent context and a time.Time value to the context.WithDeadline function. The function returns a new context and a cancel function. If the deadline is exceeded, the new context will be automatically canceled. This is similar to setting a deadline for yourself and being reminded to finish the task by a set time. The new context can also be canceled manually by calling the cancel function, similar to context.WithCancel. Note that canceling the context will only affect the new context and any others that use it as a parent context.

Giving a Context a Time Limit

The context.WithTimeout function is a convenient option that helps to simplify the process of setting a deadline for a context. Instead of having to provide a specific time.Time for the context to end, as is the case with context.WithDeadline, context.WithTimeout only requires a time.Duration value that indicates how long the context should last. While context.WithTimeout will suffice for most use cases, context.WithDeadline is still available for those instances where you need to specify a time.Time. If context.WithTimeout is not used, you would have to manually calculate the deadline by using time.Now() and the time.Time's Add method.

Context with Value

Now let's create a context with a value that can store additional information.

Output:

We simply built a new cntx object in the above program using the context package. The context.Background function returns a non-nil, empty Context.

Adding Values to the Context

We basically wrapped the old content with a new context that contains our API-key for demonstration purposes in the enrichContext function above by taking the original content and passing it through.

Note : It should be kept in mind that the WithValue function generates a new context based on the existing one and does not alter the original context.

Reading Values from the Context

The function doSomething() has been provided with a new cntx struct, and it has been determined that the "api-key" from that context is needed to access an API endpoint that requires authentication.

When extracting data from the cntx struct, it's important to keep in mind that if you try to access a key/value pair that doesn't exist, it will return nil. If you need a value from a context, check if apiKey!= nil and then return with an error if the key you need is not set.

Context Errors

The context also includes a method called Err() which is useful for returning the error that caused the function to stop. If the Done channel has not been closed, calling the Err() function will return nil. On the other hand, if Done has been closed, Err will return the error that caused it to close.

Output:

Conclusion

  • Context is a Golang standard package that allows you to easily pass request-scoped variables, cancellation signals, and deadlines to all the goroutines involved in handling a request across API boundaries.
  • The go context package can be helpful while interacting with APIs and slow processes, specifically in production-grade systems that handle web requests, where you might want to notify all the goroutines to stop work and return.
  • You can easily create an empty context using either context.TODO() function or context.Background() function.
  • Generally, there are four different ways to end a context and these are as follows:
    1. Determining if a Context is Done
    2. Canceling a Context
    3. Giving a Context a Deadline
    4. Giving a Context a Time Limit
  • The context also includes a method called Err() which is useful for returning the error that caused the function to stop.