Home Exploring Context in Golang
Post
Cancel

Exploring Context in Golang

Exploring Context in Golang

Concurrency is an important part of Go programming, and managing goroutines effectively is vital for creating strong and scalable applications. The context package in Go offers a useful tool for handling timeouts, cancellations, and sharing values specific to a request. In this blog post, we will explore the concept of context in Go, its significance, how to create and use contexts, and provide real-life examples that demonstrate how context can solve common challenges in concurrent programming.

What is Context in go and why it is needed

In Go, a context is a mechanism for managing concurrent operations, such as goroutines, by passing information and signals between different parts of a program. It allows for handling timeouts, cancellations, and sharing values in a controlled manner.

For example, in a web server, a context can be created for each incoming request and passed to the corresponding goroutine. This context can carry request-specific data and control aspects like deadlines and cancellations.

Let’s consider a scenario where you’re preparing a sandwich for a friend, and you’ve assigned a few people to buy the required ingredients like tomatoes and bread. However, your friend suddenly changes their mind and no longer wants the sandwich. In this case, you need to cancel all the ongoing operations related to buying the ingredients.

The context also supports timeouts, ensuring that if an operation takes too long, it is automatically canceled. This helps prevent unnecessary delays and ensures efficient execution.

Overall, a context in Go enables better management of concurrent operations by providing a structured way to share information, handle timeouts, and propagate cancellations. It enhances control and coordination in concurrent programming.

Context Creation

Go provides several functions to create contexts.

When working with contexts in Go, there is a concept of a parent context and a child context. The parent context serves as the root context from which child contexts are derived.

Let’s understand this concept in the context of the functions used to create contexts.

  1. context.Background() : This function returns a background context, which serves as the root parent context. It is often used as the starting point for creating other contexts.
  2. context.TODO() : The TODO function returns a context that is similar to Background() . It indicates that the specific context to use is not yet determined. It is typically used when a context is expected but not available at the moment. It is advisable to document the reason for using TODO and replace it with an appropriate context later.
  3. context.WithValue(parentContext, key, value) :This function creates a child context derived from a parent context ( parentContext ) . It associates a key-value pair with the child context, allowing for the passing of request-scoped values. The child context inherits the values from the parent context and can add or overwrite values specific to itself.
  4. context.WithTimeout(parentContext, timeout) : WithTimeout creates a child context derived from a parent context ( parentContext ) with a specified timeout duration. This child context is automatically canceled when the timeout duration elapses. The timeout value is specific to the child context and does not affect the parent context or other child contexts derived from the same parent.
  5. context.WithDeadline(parentContext, deadline) : WithDeadline creates a child context derived from a parent context ( parentContext ) with an explicit deadline. The child context is automatically canceled when the specified deadline is reached. Similar to WithTimeout , the deadline is specific to the child context and does not affect other contexts.

Context Interface

The context package defines the Context interface, which represents a context in Go. The Context interface includes the following methods:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// A Context carries a deadline, a cancellation signal, and other values across
// API boundaries.
//
// Context's methods may be called by multiple goroutines simultaneously.
type Context interface {
	// Deadline returns the time when work done on behalf of this context
	// should be canceled. Deadline returns ok==false when no deadline is
	// set. Successive calls to Deadline return the same results.
	Deadline() (deadline time.Time, ok bool)
	// Done returns a channel that's closed when work done on behalf of this
	// context should be canceled. Done may return nil if this context can
	// never be canceled. Successive calls to Done return the same value.
	// The close of the Done channel may happen asynchronously,
	// after the cancel function returns.
	// WithCancel arranges for Done to be closed when cancel is called;
	// WithDeadline arranges for Done to be closed when the deadline
	// expires; WithTimeout arranges for Done to be closed when the timeout
	// elapses.
	//
	// Done is provided for use in select statements
	Done() <-chan struct{}
	// If Done is not yet closed, Err returns nil.
	// If Done is closed, Err returns a non-nil error explaining why:
	// Canceled if the context was canceled
	// or DeadlineExceeded if the context's deadline passed.
	// After Err returns a non-nil error, successive calls to Err return the same error.
	Err() error
	// Value returns the value associated with this context for key, or nil
	// if no value is associated with key. Successive calls to Value with
	// the same key returns the same result.
	Value(key any) any
}
  • Deadline() (deadline time.Time, ok bool) : Returns the context’s deadline, indicating when the associated operation should be completed. The ok flag indicates if a deadline is set.
  • Done() &lt;-chan struct{} : Returns a channel that is closed when the context is canceled or times out.
  • Err() error : Returns the reason for context cancellation, which can be a timeout or a specific error value.
  • Value(key interface{}) interface{} : Returns the value associated with a given key from the context. This allows for passing request-scoped values through the context.

Context propogate from parent to child

When a parent goroutine creates a child goroutine in Go, the context can be propagated from the parent to the child. Context propagation allows the child goroutine to inherit and carry forward the same context values, deadlines, and cancellations.

In Go, context propagation is achieved through the use of the context.Context type, which is passed as an argument to functions or goroutines that need access to the context.

Context Hierarchy

Context Hierarchy

In Go, context propagation is achieved through the use of the context.Context type, which is passed as an argument to functions or goroutines that need access to the context.

Here’s a step-by-step explanation of how context propagates from a parent to a child goroutine:

  1. Parent Goroutine creates a Context: The parent goroutine creates a context.Context using one of the context creation functions, such as context.Background() , context.TODO() , or by deriving a new context from an existing context using functions like context.WithValue , context.WithTimeout , or context.WithDeadline .
  2. Parent Goroutine spawns a Child Goroutine: Once the context is created, the parent goroutine spawns a child goroutine using the go keyword or any other means of concurrent execution. The child goroutine is passed the context as an argument.
  3. Child Goroutine Receives the Context: In the child goroutine, the context passed from the parent goroutine is received as a parameter. This allows the child goroutine to access the same context and any associated values, deadlines, or cancellations.
  4. Context Operations in Child Goroutine: Inside the child goroutine, the context can be used to check for deadlines, retrieve values associated with keys using ctx.Value , or check for cancellation using ctx.Done() .
  5. Context Propagation in Subsequent Child Goroutines: If the child goroutine further spawns additional goroutines, the same context can be propagated by passing it to the newly created goroutines. This ensures that the context and its properties are available throughout the goroutine hierarchy.

By propagating the context from parent to child goroutines, you ensure that important information, such as deadlines or cancellations, is carried forward and can be appropriately utilized in each goroutine.

For instance, in above diagram canceling c2 will cancel c2, c4 and c5 only.

It’s important to note that while the context itself is immutable, you can derive new contexts from existing ones with additional values, timeouts, or deadlines as needed. This allows each goroutine to have its own specific context while still inheriting the values and properties of its parent context.

Real-Life Examples:

  1. WithValue Context

User Authentication: Checking if a token is valid and setting an “authenticated” value in the context to represent the authentication status of a user.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package main

import (
	"context"
	"fmt"
	"time"
)

func Authenticate(ctx context.Context, token string) bool {
	validToken := "secret_token"
	fmt.Println("-------request ID ------", ctx.Value("requestID"))
	ctx = context.WithValue(ctx, "authenticated", false)
	if token == validToken {
		ctx = context.WithValue(ctx, "authenticated", true)
	}
	return ctx.Value("authenticated").(bool)
}

func main() {
	ctx := context.Background()
	ctx = context.WithValue(ctx, "requestID", "12345")
	go func(ctx context.Context) {
		isAuthenticated := Authenticate(ctx, "secret_token")
		fmt.Println("Authenticated:", isAuthenticated)
	}(ctx)
	ctx = context.WithValue(ctx, "requestID", "12346")
	go func(ctx context.Context) {
		isAuthenticated := Authenticate(ctx, "secret_token")
		fmt.Println("Authenticated:", isAuthenticated)
	}(ctx)

	// ...
	// Perform other concurrent operations
	// ...
	time.Sleep(1 * time.Second)
}

output

output

Authenticate function checks if a token is valid and sets the authenticated value in the context accordingly. The main function creates a background context using context.Background() and sets a request ID value. The Authenticate function is invoked as a goroutine, allowing concurrent authentication checks while using the same context across multiple goroutines.

2. WithTimeout Context

External API Request : Making an API request with a timeout, where the request either completes within the specified timeout or gets canceled if it exceeds the timeout duration.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package main

import (
	"context"
	"fmt"
	"time"
)

func makeAPIRequest(ctx context.Context) {
	select {
	case <-ctx.Done():
		fmt.Println(" API request cancelled ", ctx.Err())
	case <-time.After(3 * time.Second):
		fmt.Println("API request completed")
	}
}

func main() {
	parentCtx := context.Background()
	childCtx1, cancel := context.WithTimeout(parentCtx, 2*time.Second)
	defer cancel()

	go makeAPIRequest(childCtx1)
	childCtx2, cancel := context.WithTimeout(parentCtx, 10*time.Second)
	defer cancel()
	go makeAPIRequest(childCtx2)
	// ...
	// Perform other concurrent operations
	// ...
	time.Sleep(5 * time.Second)
}

Context Hierarchy

Context Hierarchy

In this example, the MakeAPIRequest function that simulates making an API request to an external endpoint. The function uses a select statement with a timeout of 3 seconds. If the request completes within the specified timeout, it prints a success message. Otherwise, if the context is canceled due to the timeout, it prints a cancellation message.

Here childCtx1 will be cancelled as timeout is 2 seconds and childCtx2 will be completed in the given timeout.

Output

Output

3. WithDeadline Context

Task Scheduling : Scheduling a task to be completed before a given deadline, where the task either completes within the deadline or gets canceled if it exceeds the deadline.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package main

import (
	"context"
	"fmt"
	"time"
)

func ScheduleTask(ctx context.Context, taskName string) {

	select {
	case <-time.After(4 * time.Second):
		fmt.Printf("Task '%s' completed\n", taskName)
	case <-ctx.Done():
		fmt.Printf("Task '%s' canceled: %v\n", taskName, ctx.Err())
	}
}

func main() {
	parentCtx := context.Background()
	deadline := time.Now().Add(1 * time.Second)
	childCtx1, cancel := context.WithDeadline(parentCtx, deadline)
	defer cancel()

	go ScheduleTask(childCtx1, "Data Processing")

	childChildCtx1, cancel := context.WithDeadline(childCtx1, time.Now().Add(100*time.Second))
	defer cancel()
	go ScheduleTask(childChildCtx1, "File Processing")

	childCtx2, cancel := context.WithDeadline(parentCtx, time.Now().Add(10*time.Second))
	defer cancel()
	go ScheduleTask(childCtx2, "Some remote Processing")
	// ...
	// Perform other concurrent operations
	// ...
	time.Sleep(10 * time.Second)
}

Context Hierarchy

Context Hierarchy

Here I am cancelling childCtx1(data processing ctx) but it is cancelling childchildCtx1 (file processing) as well even though deadline is 100 sec for that.

output

output

Conclusion

Contexts in Go provide a standardized way to manage goroutines, handle timeouts, cancellations, and share request-scoped values.

If you would like to view the full codebase, please visit the repository by clicking here: Repo

Post converted from Medium by ZMediumToMarkdown.

This post is licensed under CC BY 4.0 by the author.