Go Context
Introduction
When working with concurrent operations in Go, you often need a way to manage and coordinate multiple goroutines. This is where Go's context
package becomes invaluable. Introduced in Go 1.7, the context
package provides a standard way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and between processes.
In this tutorial, we'll explore how to use the Go context
package to:
- Cancel long-running operations
- Set deadlines and timeouts
- Pass request-scoped values between functions
- Handle concurrency in a clean, structured way
What is Context?
A Context
in Go is an interface that contains deadlines, cancellation signals, and request-scoped values. The primary purpose of a Context
is to tell a blocking operation to abandon its work when it's no longer needed.
Let's look at the Context
interface definition:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key interface{}) interface{}
}
Deadline()
: Returns the time when the context will be canceled (if a deadline is set)Done()
: Returns a channel that's closed when the context is canceledErr()
: Returns the error explaining why the context was canceledValue()
: Returns the value associated with a key
Creating Contexts
The context
package provides several functions to create new contexts:
1. Background Context
context.Background()
is a starting point for all contexts. It's never canceled, has no values, and has no deadline.
ctx := context.Background()
2. TODO Context
context.TODO()
is similar to Background()
but indicates that we're not sure which context to use yet.
ctx := context.TODO()
3. WithCancel Context
WithCancel
creates a new context that can be canceled manually.
ctx, cancel := context.WithCancel(context.Background())
// Don't forget to call cancel when you're done
defer cancel()
4. WithTimeout Context
WithTimeout
creates a context that will be automatically canceled after a specified duration.
// Create a context that will be canceled after 5 seconds
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
5. WithDeadline Context
WithDeadline
creates a context that will be canceled at a specific time.
// Create a context that will be canceled at a specific time
deadline := time.Now().Add(10 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
6. WithValue Context
WithValue
creates a context with a key-value pair that can be passed through a call chain.
ctx := context.WithValue(context.Background(), "userId", "12345")
Context Cancellation
Let's see how we can use context to cancel operations. Here's a simple example:
package main
import (
"context"
"fmt"
"time"
)
func main() {
// Create a context with cancellation capability
ctx, cancel := context.WithCancel(context.Background())
// Start a goroutine that does some work
go worker(ctx, "Task 1")
// Let the worker run for 3 seconds, then cancel it
time.Sleep(3 * time.Second)
cancel()
// Wait a bit to see the effect of cancellation
time.Sleep(1 * time.Second)
fmt.Println("Main function exiting")
}
func worker(ctx context.Context, name string) {
for {
select {
case <-ctx.Done():
fmt.Printf("%s: Work canceled
", name)
return
case <-time.After(1 * time.Second):
fmt.Printf("%s: Working...
", name)
}
}
}
Output:
Task 1: Working...
Task 1: Working...
Task 1: Working...
Task 1: Work canceled
Main function exiting
In this example:
- We create a context with cancellation capability
- Start a worker goroutine that checks for cancellation signals
- After 3 seconds, we cancel the context
- The worker detects the cancellation and exits gracefully
Using Timeouts
Timeouts are crucial when working with external services like databases or APIs. Here's how to use WithTimeout
:
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()
// Try to perform a task that takes longer than the timeout
result, err := performTask(ctx)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Result:", result)
}
func performTask(ctx context.Context) (string, error) {
// Simulate a task that takes 3 seconds
select {
case <-time.After(3 * time.Second):
return "Task completed", nil
case <-ctx.Done():
return "", ctx.Err() // This will return context.DeadlineExceeded
}
}
Output:
Error: context deadline exceeded
This code shows:
- How to set a timeout for an operation
- How to check if the context is done before the operation completes
- How to return the appropriate error when the context expires
Passing Values with Context
Let's see how to use context to pass values through function calls:
package main
import (
"context"
"fmt"
)
func main() {
// Create a context with a value
ctx := context.WithValue(context.Background(), "userId", "12345")
// Pass the context to other functions
processRequest(ctx)
}
func processRequest(ctx context.Context) {
// Retrieve the value from the context
userId, ok := ctx.Value("userId").(string)
if !ok {
fmt.Println("No user ID found in context")
return
}
fmt.Printf("Processing request for user: %s
", userId)
// Pass the context further down the call stack
fetchUserData(ctx)
}
func fetchUserData(ctx context.Context) {
// Again, retrieve the value from the context
userId, ok := ctx.Value("userId").(string)
if !ok {
fmt.Println("No user ID found in context")
return
}
fmt.Printf("Fetching data for user: %s
", userId)
}
Output:
Processing request for user: 12345
Fetching data for user: 12345
This example demonstrates:
- How to add a value to a context
- How to retrieve values from the context in different functions
- How context values propagate through the call chain
Real-World Example: HTTP Server with Timeouts
Let's see a practical example of using context in an HTTP server:
package main
import (
"context"
"fmt"
"log"
"net/http"
"time"
)
func main() {
// Create a simple HTTP server
http.HandleFunc("/", handleRequest)
// Start the server
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func handleRequest(w http.ResponseWriter, r *http.Request) {
// Get the context from the request with a timeout
ctx, cancel := context.WithTimeout(r.Context(), 2*time.Second)
defer cancel()
// Start a goroutine to process the request
resultChan := make(chan string, 1)
go func() {
// Simulate a database query or API call
time.Sleep(3 * time.Second)
resultChan <- "Request processed successfully"
}()
// Wait for either the work to complete or the context to timeout
select {
case result := <-resultChan:
fmt.Fprintln(w, result)
case <-ctx.Done():
log.Println("Request timed out or canceled")
http.Error(w, "Request timed out", http.StatusGatewayTimeout)
}
}
When you run this server and make a request to http://localhost:8080, it will timeout after 2 seconds and return a 504 Gateway Timeout error, because our simulated work takes 3 seconds.
This demonstrates how to:
- Use context in an HTTP server
- Set timeouts for handling requests
- Cancel long-running operations when the client disconnects or timeout occurs
Context Propagation
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)