Context Package: Cancellation and Timeouts
The context package is essential for managing cancellation, timeouts, and deadlines in Go applications. It provides a clean way to propagate cancellation signals through your codebase.
Context Basics
Context carries deadlines, cancellation signals, and request-scoped values.
Good: Using Context
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Work cancelled:", ctx.Err())
return
default:
fmt.Println("Working...")
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
go doWork(ctx)
time.Sleep(500 * time.Millisecond)
cancel()
time.Sleep(100 * time.Millisecond)
}
Bad: Manual Cancellation
// โ AVOID: Manual cancellation without context
package main
import (
"fmt"
"time"
)
func doWork(done chan bool) {
for {
select {
case <-done:
fmt.Println("Work cancelled")
return
default:
fmt.Println("Working...")
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
done := make(chan bool)
go doWork(done)
time.Sleep(500 * time.Millisecond)
close(done)
time.Sleep(100 * time.Millisecond)
}
Context Types
WithCancel
Cancel context manually.
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d stopped\n", id)
return
default:
fmt.Printf("Worker %d working\n", id)
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
time.Sleep(500 * time.Millisecond)
cancel()
wg.Wait()
}
WithTimeout
Automatically cancel after duration.
package main
import (
"context"
"fmt"
"time"
)
func fetchData(ctx context.Context) (string, error) {
select {
case <-time.After(2 * time.Second):
return "data", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
// Timeout after 1 second
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
data, err := fetchData(ctx)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Data:", data)
}
}
WithDeadline
Cancel at specific time.
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("Deadline exceeded:", ctx.Err())
return
default:
fmt.Println("Working...")
time.Sleep(100 * time.Millisecond)
}
}
}
func main() {
deadline := time.Now().Add(500 * time.Millisecond)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
go doWork(ctx)
time.Sleep(1 * time.Second)
}
WithValue
Attach request-scoped values.
package main
import (
"context"
"fmt"
)
type userKey string
const userIDKey userKey = "userID"
func processRequest(ctx context.Context) {
userID := ctx.Value(userIDKey)
fmt.Printf("Processing request for user: %v\n", userID)
}
func main() {
ctx := context.WithValue(context.Background(), userIDKey, 12345)
processRequest(ctx)
}
Propagating Context
Pass context through function calls.
Good: Context Propagation
package main
import (
"context"
"fmt"
"time"
)
func level3(ctx context.Context) {
select {
case <-ctx.Done():
fmt.Println("Level 3: Context cancelled")
return
case <-time.After(100 * time.Millisecond):
fmt.Println("Level 3: Work complete")
}
}
func level2(ctx context.Context) {
fmt.Println("Level 2: Starting")
level3(ctx)
fmt.Println("Level 2: Done")
}
func level1(ctx context.Context) {
fmt.Println("Level 1: Starting")
level2(ctx)
fmt.Println("Level 1: Done")
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
level1(ctx)
}
Bad: Not Propagating Context
// โ AVOID: Not passing context through call chain
package main
import (
"fmt"
"time"
)
func level3() {
time.Sleep(100 * time.Millisecond)
fmt.Println("Level 3: Work complete")
}
func level2() {
fmt.Println("Level 2: Starting")
level3()
fmt.Println("Level 2: Done")
}
func level1() {
fmt.Println("Level 1: Starting")
level2()
fmt.Println("Level 1: Done")
}
func main() {
// No way to cancel level3 work
level1()
}
Timeout Patterns
HTTP Request with Timeout
package main
import (
"context"
"fmt"
"io"
"net/http"
"time"
)
func fetchURL(ctx context.Context, url string) (string, error) {
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return "", err
}
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
return string(body), err
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
data, err := fetchURL(ctx, "https://example.com")
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Printf("Fetched %d bytes\n", len(data))
}
}
Database Query with Timeout
package main
import (
"context"
"fmt"
"time"
)
// Simulated database query
func queryDatabase(ctx context.Context, query string) (string, error) {
select {
case <-ctx.Done():
return "", ctx.Err()
case <-time.After(2 * time.Second):
return "result", nil
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
result, err := queryDatabase(ctx, "SELECT * FROM users")
if err != nil {
fmt.Println("Query error:", err)
} else {
fmt.Println("Result:", result)
}
}
Goroutine Coordination
Use context to coordinate multiple goroutines.
Good: Coordinated Cancellation
package main
import (
"context"
"fmt"
"sync"
"time"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("Worker %d: Cancelled\n", id)
return
case <-time.After(100 * time.Millisecond):
fmt.Printf("Worker %d: Working\n", id)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
time.Sleep(500 * time.Millisecond)
fmt.Println("Cancelling all workers...")
cancel()
wg.Wait()
fmt.Println("All workers stopped")
}
Context with Values
Good: Request-Scoped Values
package main
import (
"context"
"fmt"
)
type requestIDKey string
const requestID requestIDKey = "requestID"
func handleRequest(ctx context.Context) {
id := ctx.Value(requestID)
fmt.Printf("Handling request: %v\n", id)
processData(ctx)
}
func processData(ctx context.Context) {
id := ctx.Value(requestID)
fmt.Printf("Processing data for request: %v\n", id)
}
func main() {
ctx := context.WithValue(context.Background(), requestID, "req-12345")
handleRequest(ctx)
}
Bad: Global State
// โ AVOID: Using global variables for request data
package main
import (
"fmt"
)
var currentRequestID string
func handleRequest(id string) {
currentRequestID = id
fmt.Printf("Handling request: %s\n", currentRequestID)
processData()
}
func processData() {
fmt.Printf("Processing data for request: %s\n", currentRequestID)
}
func main() {
handleRequest("req-12345")
}
Error Handling with Context
Handling Context Errors
package main
import (
"context"
"fmt"
"time"
)
func doWork(ctx context.Context) error {
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(2 * time.Second):
return nil
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second)
defer cancel()
err := doWork(ctx)
if err != nil {
switch err {
case context.Canceled:
fmt.Println("Work was cancelled")
case context.DeadlineExceeded:
fmt.Println("Work exceeded deadline")
default:
fmt.Println("Error:", err)
}
}
}
Best Practices
- Always Pass Context: Pass context as first parameter
- Respect Cancellation: Check context.Done() regularly
- Use Appropriate Timeout: Set reasonable timeouts
- Don’t Store Context: Don’t store context in structs
- Create Child Contexts: Use WithCancel, WithTimeout, etc.
- Handle Errors: Check for context.Canceled and context.DeadlineExceeded
- Use Values Sparingly: Only for request-scoped data
- Defer Cancel: Always defer cancel() to prevent leaks
Common Pitfalls
- Ignoring Context: Not checking context.Done()
- Storing Context: Storing context in struct fields
- No Timeout: Not setting timeouts for operations
- Leaking Goroutines: Not cancelling context properly
- Overusing Values: Using context for too much data
Resources
Summary
The context package is essential for managing cancellation, timeouts, and request-scoped values in Go. Always pass context as the first parameter, respect cancellation signals, and set appropriate timeouts. Use context to coordinate goroutines and propagate cancellation through your application gracefully.
Comments