DEV Community

Umang Mundhra
Umang Mundhra

Posted on

Understanding and Utilizing Context in Go and GoFr

Context in go

What is Context?

In Go, the context package is a fundamental tool designed to manage and share request-scoped data, cancellation signals, and timeouts or deadlines across different layers of an application.
The context.Context type provides a way to carry deadlines, cancellation signals, and other request-scoped values in a chainable, hierarchical manner. It is designed to be immutable, which means that each new derived context carries the properties of its parent context while adding new values or behaviours.
Imagine a context as a special carrier bag that holds all the necessary information and instructions for a specific task. This bag can be passed from function to function, ensuring everyone involved has the same context (information) and can act accordingly. It can also contain a self-destruct button (cancellation signal) to stop the operation if needed.


Why do we need Context?

Before the introduction of Context in Go, managing request-scoped data and cancellation signals was a cumbersome task. Developers had to rely on ad-hoc solutions, such as passing around custom structs or using global variables, to share data and signals between functions and goroutines. However, these approaches were error-prone, hard to maintain, and often led to tight coupling between components.
The need for a Context arises from the fact that many operations in a program are asynchronous, concurrent, or dependent on external systems. In such cases, it's essential to have a way to propagate cancellation signals, deadlines, and request-scoped data across multiple functions and goroutines.

Let's consider a few scenarios to illustrate the need for a Context:

1. The Restaurant Analogy

Imagine you're the person taking orders in a bustling restaurant. When an order arrives, you assign it to one of your skilled cooks. But what if the customer suddenly decides to leave? Without hesitation, you'd inform the chef to stop processing that order to avoid wasting ingredients. This scenario mirrors how context works in Go.

  • Cancellation Propagation: In the restaurant analogy, the customer leaving corresponds to a cancellation signal. Similarly, in Go, context allows graceful propagation of cancellation signals. When a parent operation (e.g., an HTTP request handler) cancels, all related child operations (e.g., database queries or API calls) terminate promptly. This prevents resource leaks and ensures efficient cleanup.

2. Handling Slow APIs and Timeouts

Suppose your application makes web requests to external APIs or runs system commands. In production-grade systems, it's essential to set timeouts. Here's why:

  • API Dependency: Imagine your service relies on an external API. If that API is running slowly or becomes unresponsive, you wouldn't want your system to back up with pending requests. A timeout ensures that if the API call exceeds a specified duration, the associated context cancels, allowing your application to gracefully handle the situation.

  • Performance Degradation: Without timeouts, slow external
    dependencies can lead to a cascading effect. Backlogged requests increase load, degrade performance, and impact overall system responsiveness. By using context with deadlines, you can prevent this issue.



Uses of Context:

The context package in Go is versatile and used in various scenarios to manage request-scoped data, cancellation signals, and timeouts.

Here are some common real-world use cases:

1. Propagating Trace IDs Across Distributed Services
In a microservices architecture, tracing requests across different services is crucial for monitoring and debugging. The context package allows you to propagate trace IDs and other metadata across service boundaries, ensuring consistent logging and tracing information throughout the request's lifecycle.

2. Passing Authentication Tokens
In many applications, authentication tokens or user information needs to be passed through different layers of the application. The context package provides a clean way to handle this, ensuring that authentication information is available where needed without polluting function signatures with additional parameters.

3. WebSocket and Streaming APIs
In WebSocket and streaming APIs, Context is used to manage the lifetime of the connection and propagate request-scoped data, such as the user ID and session information. This allows you to implement features like authentication, rate limiting, and session management efficiently.


Different Types of Context in Go:

The context package in Go offers several ways to create and manipulate context objects. Here's a breakdown of the commonly used types:

1. Background Context/TODO Context

This is the most basic context and serves as an empty starting point. It doesn't carry any cancellation signal or deadline. Use it when no specific context is required.
The only time TODO is used instead of Background is when the implementation is unclear, or the context is not yet known.

Typically used in main functions, initialization, and tests.

ctx := context.Background()
ctx2 := context.TODO()

2. context.WithCancel

Creates a new context derived from a parent context., WithCancel method returns a copy of the parent context along with a cancel function; invoking the cancel function releases resources connected with the context and should be called as soon as operations in the Context type are finally completed.

Used to cancel operations in a goroutine when certain conditions are met.

parentCtx := context.Background()
ctx, cancel := context.WithCancel(parentCtx)
defer cancel() // Clean up resources

3. context.WithDeadline

Similar to setting a deadline for oneself, you can set a deadline for context. Go will automatically cancel the context for you if the time limit you specified for it to finish is reached. It creates a new context derived from a parent context with a specific deadline.

Useful when you have a specific deadline for completing an operation.

parentCtx := context.Background()
deadline := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(parentCtx, deadline)
defer cancel() // Clean up resources

4. context.WithValue

Creates a new context derived from a parent context with an associated key-value pair. This allows you to store and retrieve request-specific information within the context. WithValue accepts a parent context and returns a context copy. As a result, rather than overwriting the value, it creates a new duplicate with a new key-value pair.

Useful for passing request-scoped data through the context, such as authentication tokens or trace IDs.

parentCtx := context.Background()
userIDKey := "auth-token"
ctx := context.WithValue(parentCtx, userIDKey, "abc123")

5. context.WithTimeout

Creates a context with an associated timeout. The context cancels automatically after the specified duration. WithTimeout, allows a program to continue where it might otherwise hang, giving the end user a better experience. It accepts a brief period as a parameter, along with the parent context, and terminates the function if it runs beyond the timeout period.

Useful for setting deadlines on operations.

parentCtx := context.Background()
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)
defer cancel() // Clean up resources

Different types of context in Golang



GoFr Context: Supercharged Context for Go Applications

Now that we've explored the power of context in Go, let's delve into how GoFr takes it a step further with GoFr Context.

GoFr is an opinionated microservices development framework built on top of Go. It aims to streamline the development process by providing a structured approach to building robust and scalable microservices.

GoFr Context beautifully extends the functionality of the standard Go context package by creating a wrapper around it. This wrapper provides a rich set of pre-configured dependencies directly accessible through the context object. This fully loaded context enables developers to seamlessly access logging, database connections, underlying services, traces, and metrics - all from a single context object.

Example

func MyHandler(c *gofr.Context) (interface{}, error) {
  // Logging
  c.Info("Processing request...")

  // Database access
  var value int
  err := c.SQL.QueryRowContext(c, "select 2+2").Scan(&value)
  if err != nil {
    return nil, datasource.ErrorDB{Err: err, Message: "error from sql db"}
  }

  // Accessing another service
  resp, err := c.GetHTTPService("anotherService").Get(c, "book", nil)
  if err != nil {
    return nil, err
  }

  // Creating a tracing span
  span := c.Trace("some-sample-work")
  defer span.End()
  // ... perform some work

  return value, nil
}
Enter fullscreen mode Exit fullscreen mode

In this example, notice how we can access functionalities like logging (c.Info), database interaction (c.SQL), service calls (c.GetHTTPService), and tracing (c.Trace) directly through the GoFr Context object (c).

The GoFr context is available in all HTTP handlers, greatly improving the development experience by providing a centralized way to manage different aspects of the application. This integration ensures that developers can focus on business logic while leveraging GoFr's capabilities for effective context management.


Conclusion

When it comes to program design, the Golang context package is an extremely useful tool. Context in Go serves as a powerful tool for managing request-scoped data, cancellation signals, and deadlines. Whether you're building microservices, APIs, or web applications, context ensures clean resource handling and consistent behaviour.

Do checkout GoFr and it's Github Repo and support it by giving it a ⭐.

Top comments (0)