Understanding Context in Go
In Go, the context
package provides a way to carry deadlines, cancellation signals, and other request-scoped values across API boundaries. It is commonly used in applications where you need to manage timeouts, cancellations, or pass additional information between functions or goroutines.
Here’s an easy and detailed explanation with documented code examples.
Why context
?
When you’re dealing with goroutines or HTTP requests, you may need:
- Cancellation: Stop ongoing work when a parent operation is canceled (e.g., user closed the browser tab).
- Timeout: Limit how long an operation can run.
- Shared Values: Pass values like user IDs or configurations without changing function signatures too much.
The context
package helps solve these problems.
Basics of context
The context
package provides the following:
- Context types:
context.Background()
: The root context, often used as the starting point.context.TODO()
: A placeholder context when you’re unsure what to use.
- Derived contexts:
WithCancel
: To cancel operations manually.WithTimeout
: To cancel after a set timeout.WithDeadline
: To cancel at a specific time.WithValue
: To carry values through the context.
Example 1: Basic context
Usage with Cancellation
Suppose you want to stop a goroutine when a user cancels an operation.
package main
import (
"context"
"fmt"
"time"
)
func main() {
// Create a context with cancellation
ctx, cancel := context.WithCancel(context.Background())
go func() {
for {
select {
case <-ctx.Done(): // Check if the context is canceled
fmt.Println("Operation canceled!")
return
default:
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}()
time.Sleep(2 * time.Second)
cancel() // Cancel the context
time.Sleep(1 * time.Second) // Allow goroutine to finish
}
Explanation:
context.WithCancel
creates a cancelable context.ctx.Done()
returns a channel that is closed when the context is canceled.- The goroutine listens for cancellation and stops working when it detects it.
Example 2: Using WithTimeout
You can set a timeout for an operation, automatically canceling it when time runs out.
package main
import (
"context"
"fmt"
"time"
)
func main() {
// Create a context with a 2-second timeout
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel() // Clean up resources
go func() {
select {
case <-ctx.Done():
fmt.Println("Timeout reached:", ctx.Err())
}
}()
time.Sleep(3 * time.Second)
fmt.Println("Main function ends")
}
Explanation:
context.WithTimeout
automatically cancels the context after the specified duration.ctx.Err()
provides the reason for cancellation (context.DeadlineExceeded
in this case).
Example 3: Passing Values with Context
You can use WithValue
to pass data through the context.
package main
import (
"context"
"fmt"
)
func main() {
// Add a value to the context
ctx := context.WithValue(context.Background(), "userID", 42)
printUserID(ctx)
}
func printUserID(ctx context.Context) {
// Retrieve the value from the context
userID := ctx.Value("userID")
if userID != nil {
fmt.Println("User ID:", userID)
} else {
fmt.Println("No User ID found")
}
}
Explanation:
WithValue
stores a key-value pair in the context.- Use
ctx.Value(key)
to retrieve the value later.
⚠️ Note: Avoid overusing
WithValue
as it can make code harder to understand and debug.
Summary of Best Practices
- Use context responsibly: Don’t pass context to everywhere unnecessarily.
- Don’t store large data: Context should not be used for large objects like files.
- Pass context as the first parameter: Follow Go conventions (
func doSomething(ctx context.Context, ...)
).