Understanding Context in Go Programming
The context package provides a way to carry deadlines, cancellation signals, and request-scoped values across API boundaries and between processes. This guide covers everything you need to know about using context effectively.
Context Basics
Creating Contexts
// Background context
ctx := context.Background()
// TODO context (for when you're not sure what context to use)
ctx := context.TODO()
// Context with cancellation
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Context with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Context with deadline
deadline := time.Now().Add(5 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()
Context Values
// Adding values to context
ctx := context.WithValue(context.Background(), "key", "value")
// Retrieving values
value := ctx.Value("key").(string)
// Type-safe context keys
type contextKey string
const userKey contextKey = "user"
ctx = context.WithValue(ctx, userKey, User{Name: "Alice"})
user := ctx.Value(userKey).(User)
Best Practices
1. Context Propagation
// Good: Pass context as first parameter
func ProcessRequest(ctx context.Context, req *Request) error {
// Check if context is done
select {
case <-ctx.Done():
return ctx.Err()
default:
// Process request
}
return nil
}
// Bad: Context buried in struct
type BadService struct {
ctx context.Context
}
// Good: Context passed explicitly
type GoodService struct {
// No context here
}
func (s *GoodService) Process(ctx context.Context) error {
// Use context here
return nil
}
2. Cancellation Handling
func worker(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return ctx.Err()
default:
// Do work
if err := doWork(); err != nil {
return err
}
}
}
}
// Usage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go worker(ctx)
3. Value Types
// Good: Use custom types for context keys
type key int
const (
requestIDKey key = iota
userIDKey
)
// Good: Wrap context values
type RequestScope struct {
RequestID string
UserID string
}
func WithRequestScope(ctx context.Context, rs RequestScope) context.Context {
return context.WithValue(ctx, requestIDKey, rs)
}
func GetRequestScope(ctx context.Context) (RequestScope, bool) {
rs, ok := ctx.Value(requestIDKey).(RequestScope)
return rs, ok
}
Common Patterns
1. HTTP Server
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// Add request-scoped values
ctx = context.WithValue(ctx, "requestID", uuid.New().String())
// Create child context with timeout
ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
select {
case <-ctx.Done():
http.Error(w, "Request timed out", http.StatusGatewayTimeout)
return
case result := <-processRequest(ctx):
fmt.Fprintf(w, "Result: %v", result)
}
}
func processRequest(ctx context.Context) <-chan string {
ch := make(chan string)
go func() {
defer close(ch)
// Simulate work
select {
case <-ctx.Done():
return
case <-time.After(time.Second):
ch <- "processed"
}
}()
return ch
}
2. Database Operations
type DB struct {
pool *sql.DB
}
func (db *DB) QueryWithTimeout(ctx context.Context, query string) (*sql.Rows, error) {
// Add timeout for database operations
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
// Execute query with context
rows, err := db.pool.QueryContext(ctx, query)
if err != nil {
return nil, fmt.Errorf("query failed: %w", err)
}
return rows, nil
}
3. Worker Pool
type WorkPool struct {
workers int
jobs chan Job
}
func (p *WorkPool) Start(ctx context.Context) {
for i := 0; i < p.workers; i++ {
go func() {
for {
select {
case job := <-p.jobs:
select {
case <-ctx.Done():
return
default:
job.Execute(ctx)
}
case <-ctx.Done():
return
}
}
}()
}
}
Performance Considerations
1. Context Chain Length
// Bad: Long context chain
ctx1 := context.WithValue(context.Background(), "key1", "value1")
ctx2 := context.WithValue(ctx1, "key2", "value2")
ctx3 := context.WithValue(ctx2, "key3", "value3")
// ... and so on
// Better: Group related values
type RequestContext struct {
Key1, Key2, Key3 string
}
ctx := context.WithValue(context.Background(), "request", RequestContext{
Key1: "value1",
Key2: "value2",
Key3: "value3",
})
2. Value Lookup Cost
// Expensive: Frequent context value lookups
func processRequest(ctx context.Context) {
for i := 0; i < 1000; i++ {
id := ctx.Value("requestID").(string)
// Use id
}
}
// Better: Cache frequently used values
func processRequest(ctx context.Context) {
id := ctx.Value("requestID").(string)
for i := 0; i < 1000; i++ {
// Use id
}
}
Common Mistakes
1. Storing Request Objects
// Wrong: Storing request object in context
type Request struct {
Method string
Path string
}
ctx := context.WithValue(context.Background(), "request", &Request{
Method: "GET",
Path: "/api",
})
// Right: Store only request-scoped values
ctx := context.WithValue(context.Background(), "requestID", "123")
ctx = context.WithValue(ctx, "userID", "456")
2. Context in Structs
// Wrong: Storing context in struct
type Service struct {
ctx context.Context
// other fields
}
// Right: Pass context to methods
type Service struct {
// other fields
}
func (s *Service) Process(ctx context.Context) error {
// Use context here
return nil
}
3. Cancellation Propagation
// Wrong: Not propagating cancellation
func (s *Service) Process(ctx context.Context) error {
// Create new context without parent
newCtx := context.Background()
return s.worker.Do(newCtx)
}
// Right: Propagate parent context
func (s *Service) Process(ctx context.Context) error {
// Create child context
childCtx, cancel := context.WithTimeout(ctx, time.Second)
defer cancel()
return s.worker.Do(childCtx)
}
Next Steps
- Learn about goroutines
- Explore channels
- Study select
- Practice with timeouts