Home/Blog/Programming/Understanding contexts in Go
Home/Blog/Programming/Understanding contexts in Go

Understanding contexts in Go

6 min read
Jan 22, 2024

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

The Go context package efficiently handles cancellations, timeouts, and passing data between goroutines. The package allows for the functions to have more “context” about the environment they are running in. Passing the context with function calls can propagate contextual information throughout the program’s life cycle. Context is commonly used in environments where concurrent operations’ life cycle management and cancellation are crucial, such as distributed systems.

For this blog, we expect you to have basic knowledge of Go and be familiar with the Go syntax and program structure. Context is a relatively advanced topic, requiring knowledge about goroutines and channels. If you are unfamiliar with goroutines at this stage, it is sufficient to mention that by appending go before a function call, we can make that function run concurrently in a separate thread. Here is an example:

package main
import "fmt"
import "time"
func main() {
for counter := 0; counter < 5; counter++ {
go fmt.Println(counter)
}
//give some time for other goroutine to conclude
time.Sleep(1 * time.Second)
}

From the output of the code above, we can see that the order of numbers being printed is not sequential, because multiple threads are being spawned to print them, and their order of execution is nondeterministic.

Let’s start our discussion about contexts with a simple example:

package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
go slowTask(ctx)
//give some time for the slowTask goroutine to conclude
time.Sleep(3 * time.Second)
}
func slowTask(ctx context.Context) {
select {
case <-time.After(3 * time.Second):
fmt.Println("Operation complete.")
case <-ctx.Done():
fmt.Println("Canceled:", ctx.Err())
}
}

At a glance, even a simple example may appear complicated. Fret not; we are here to help you out, and before giving a more general introduction, let’s try to understand the example above.

First, we create a context using context.Background() which returns an empty context and is often used as a root context for other contexts. We’ll explore different options for creating contexts (including empty contexts) later in the blog.

Note: Contexts can be created and derived from existing contexts, resulting in a tree-like hierarchical structure. Generally, all contexts, other than empty contexts, are derived from a parent context.

Then, we create a context using context.WithTimeout and reassign it to the ctx variable. As the name implies, it creates a context with a timeout, in our case, 2 seconds. The timeout specifies that the context will be canceled after the timeout.

After this, we start a goroutine to perform a task and, as the name suggests, it will be a slow, time-consuming task. We consider two cases in the slowTask function. Either the operation gets completed, or the time allocated to the context runs out and is canceled. Here is how they are handled:

  • We simulate a time-consuming task using time.After. If the task completes successfully within three seconds, we print the appropriate message.

  • The ctx.Done() function returns a closed channel when the context is canceled or times out. In our case, the context will timeout in two seconds, and we print the cancelation message with the reason using ctx.Err().

Creating contexts in Go#

Now that we have a basic understanding of contexts, let’s formally introduce some options for creating contexts:

  • context.Background(): We have already seen this in the example before, and it returns an empty context to be used as the root context.

  • context.TODO(): Similar to the previous function, it returns an empty context. This option is used to signal that it is a placeholder, and that it is unclear which type of context to use. In the example shown above, try replacing context.Background() with context.TODO() and the output would stay the same.

  • context.WithValue(parentContext, key, value): We have learned that the context can also be used for passing data between goroutines. This function returns a new derived context with the associated key-value pair. We’ll see an example of this context in the next section.

  • context.WithCancel(parentContext): This function returns a new derived context with a cancel function. Only the function creating the context can use the cancel function to cancel the context and any contexts derived from it. We’ll see an example of this context later in the blog.

  • context.WithTimeout(parentContext, timeout): We have used this function in the example above. The timeout parameter is of type time.Duration, and we used a timeout of two seconds in our example.

  • context.WithDeadline(parentContext, deadline): This function is similar to the one shown above. However, we can see the deadline parameter having the type time.Time as the second parameter. This function returns a new derived context with a deadline. The context is canceled when the deadline expires. In our example above, try replacing context.WithTimeout with context.WithDeadline.

Pass values through a call chain#

One common use case of the context package is to pass key-value pairs through a call chain.

Consider the case where we intend to pass along some authorization information that can be used to include authorization checks within individual functions. A sample implementation using context.WithValue is shown below:

package main
import (
"context"
"fmt"
"time"
)
type contextkey string
func main() {
ctx := context.Background()
ctx = context.WithValue(ctx, contextkey("AuthToken"), "X@cfs645JHSDcdae")
go processSensitiveData(ctx)
//give some time for other goroutine to conclude
time.Sleep(2 * time.Second)
}
func processSensitiveData(ctx context.Context) {
authToken, ok := ctx.Value(contextkey("AuthToken")).(string)
if !ok {
//print appropriate message
fmt.Println("Access Denied - missing AuthToken!")
return
}
fmt.Printf("Processing data using: %s", authToken)
}

The code above is easier to understand. However, notice the use of a custom type, contextkey, instead of directly using the type string. This is needed to avoid collisions between packages using context. You can try to modify the above code to explore the case where contextual context information for a function is missing.

Canceling a chain of function calls#

We can also use the context package to cancel a chain of function calls after a specific event. In the example below, we have used context.WithCancel(ctx) for a new derived context with a cancel function.

package main
import (
"context"
"fmt"
"time"
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
go processOrder(ctx)
time.Sleep(3 * time.Second)
fmt.Println("main: Canceling order processing!")
cancel()
//give some time for other goroutines to conclude
time.Sleep(4 * time.Second)
}
func processOrder(ctx context.Context) {
fmt.Println("Processing order...")
time.Sleep(1 * time.Second)
GetOderDetails(ctx)
// Check if cancelation signal received
if ctx.Err() != nil {
fmt.Println("Canceled: Processing order")
return
}
}
func GetOderDetails(ctx context.Context) {
// Perform some work
fmt.Println("Fetching order details...")
// Simulate a long-running task
time.Sleep(1 * time.Second)
GetInventoryDetails(ctx)
// Check if cancelation signal received
if ctx.Err() != nil {
fmt.Println("Canceled: Fetching order details")
return
}
//continue work after fetching inventory details
}
func GetInventoryDetails(ctx context.Context) {
// Perform some work
fmt.Println("Fetching inventory details...")
// Simulate a long-running task
time.Sleep(2 * time.Second)
// Check if cancelation signal received
if ctx.Err() != nil {
fmt.Println("Canceled: Fetching inventory details")
return
}
}

The example above uses various concepts learned in the blog. First, the main function creates a context and passes it to the processOrder function in line 11, and we can see that the context is passed to other functions in the call chain. It is important to remember that with context.WithCancel, only the function creating the context can use the cancel function to cancel the context and any contexts derived from it.

We have used time.Sleep to simulate processing, and for our case, the main function cancels the context in line 17, while the inventory details are being fetched. When a context is passed to a function, it should honor it by checking for its cancelation. There is a more elegant way to handle this by using select, channels, and a separate goroutine to concurrently poll for the cancel message. We leave this as an exercise for you!

Summary#

The context package allows for the functions to have more context about the environment they are running in. The use cases for the Go context package include efficiently handling cancelations, timeouts, and passing data between goroutines. Contexts can be created and derived from existing contexts, resulting in a tree-like hierarchical structure.

Context is commonly used in environments where concurrent operations’ life cycle management and cancelation are crucial, such as distributed systems. We encourage you further to enrich your knowledge and understanding of the Go language. For Go interview prep, you can follow the path below.

Cover
Ace the Go Coding Interview

Go is a programming language built for programs related to networking and infrastructure and is widely used in artificial intelligence and data science industries. Go language professionals are in high demand in the developing sectors. This Skill Path will ensure that you understand the fundamentals of Go language and are able to solve real-world problems using Go. This Path starts with the basic concepts of the Go language together with their practical implementations. Moving ahead, you'll get familiar with data structure concepts, their performance, and concurrency concepts. By the end of this Skill Path, you'll be solving various real-world problems and have acquired the necessary skills to make a good impression in any Go coding interview.

143hrs
Beginner
140 Challenges
227 Quizzes

Frequently Asked Questions

What are contexts in GoLang?

The context package, an integral part of the Go standard library, offers a suite of tools for handling concurrent processes. It facilitates the transmission of cancellation signals, deadlines, and values across goroutines. This ensures the smooth and timely termination of interconnected operations when required.


Written By:
Ehtesham Zahoor
 
Join 2.5 million developers at
Explore the catalog

Free Resources