When You Should Pass Context
Functions Performing Operations Related to a Request
If your function is part of a request-handling pipeline (e.g., an HTTP handler, a gRPC service), it should almost always accept a context.Context
. This allows you to propagate request-specific values, timeouts, and cancellation signals throughout your application.
func handleUserRequest(ctx context.Context, userID int) error {
// ... perform database operations using ctx ...
return nil
}
Functions Performing Long-Running or Potentially Blocking Operations
If a function might take a significant amount of time to complete or could block indefinitely (e.g., network calls, database queries), passing a context enables graceful cancellation.
func fetchDataFromExternalAPI(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, err
}
// ... perform HTTP request ...
return responseBody, nil
}
Functions Spawning Goroutines
When a function starts new goroutines, it’s crucial to pass the context to those goroutines. This ensures that they respect the cancellation and deadline signals of the parent operation.
func processData(ctx context.Context, data []string) error {
var wg sync.WaitGroup
errChan := make(chan error, len(data))
for _, item := range data {
wg.Add(1)
go func(item string) {
defer wg.Done()
err := processItem(ctx, item)
if err != nil {
errChan <- err
}
}(item)
}
go func() {
wg.Wait()
close(errChan)
}()
for err := range errChan {
if err != nil {
return err
}
}
return nil
}
When You Should Not Pass Context
Purely Synchronous, Short-Lived Functions
If a function performs a simple, synchronous operation that is guaranteed to complete quickly, passing a context is often unnecessary overhead. Examples include basic string manipulation, simple calculations, or data structure operations.
func calculateSum(a, b int) int {
return a + b
}
Functions with No External Dependencies
If a function doesn’t interact with external resources (e.g., databases, networks, file systems) and doesn’t spawn goroutines, there’s typically no need for a context.
Functions Used in Libraries or Packages Intended for Broad Reuse
Library functions should generally avoid requiring a context unless they specifically need it. This allows users of the library to decide how to manage context in their applications. If a library needs to provide a way to cancel operations, or pass request scoped data, it should offer function variations that accept context, rather than forcing it in all cases.
When the Context Has No Meaning
If you’re passing a context solely because “it’s good practice” without a clear purpose, you’re adding unnecessary complexity. Ensure the context provides value in terms of cancellation, deadlines, or request-scoped values.
Best Practices
-
Context as the First Parameter: Always pass the
context.Context
as the first parameter of a function. - Avoid Storing Context: Don’t store contexts in structs or global variables. They are meant to be request-scoped and short-lived.
-
Use
context.Background()
for Root Contexts: When starting a new operation without an existing context, usecontext.Background()
. -
Use
context.TODO()
for Placeholder Contexts: If you know you’ll need a context later but don’t have one yet, usecontext.TODO()
as a placeholder. -
Respect Context Cancellation: Always check
ctx.Done()
to see if the context has been canceled. -
Use Timeouts and Deadlines: Set timeouts or deadlines using
context.WithTimeout()
orcontext.WithDeadline()
to prevent operations from running indefinitely. -
Pass Relevant Values in Context: Use
context.WithValue()
to pass request-scoped values when necessary, but use it sparingly and avoid passing too much data.
Conclusion
By following these guidelines, you can effectively use the context
package in Go to write robust, maintainable, and efficient applications.