Learn Go
advanced2 min read

Context

The context package provides a standard way to carry deadlines, cancellation signals, and request-scoped values across goroutines and API boundaries. Pass a context as the first argument to every function that starts a goroutine or makes a blocking call.

The context.Context Interface

type Context interface {
    Deadline() (deadline time.Time, ok bool)
    Done() <-chan struct{}
    Err() error
    Value(key any) any
}

context.Background and context.TODO

Use context.Background() as the root context in main, tests, and top-level initialisers. Use context.TODO() as a placeholder when you are not yet sure which context to use:

package main
 
import (
    "context"
    "fmt"
)
 
func main() {
    ctx := context.Background()
    fmt.Println(ctx) // context.Background
}

WithCancel

WithCancel creates a derived context with a cancel function. Calling cancel() closes ctx.Done():

package main
 
import (
    "context"
    "fmt"
    "sync"
)
 
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
    defer wg.Done()
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("worker %d cancelled: %v\n", id, ctx.Err())
            return
        default:
            // do work
            fmt.Printf("worker %d working\n", id)
            return // simulate a single unit of work
        }
    }
}
 
func main() {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel() // always cancel to release resources
 
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go worker(ctx, i, &wg)
    }
 
    cancel() // cancel all workers
    wg.Wait()
}

Idiomatic Go: Always call the cancel function returned by WithCancel, WithTimeout, or WithDeadline. Use defer cancel() immediately after the call.

WithTimeout and WithDeadline

WithTimeout(ctx, duration) cancels the context after the given duration. WithDeadline(ctx, time) cancels it at a specific time:

package main
 
import (
    "context"
    "fmt"
    "time"
)
 
func slowFetch(ctx context.Context) (string, error) {
    select {
    case <-time.After(200 * time.Millisecond):
        return "data", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}
 
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
    defer cancel()
 
    result, err := slowFetch(ctx)
    if err != nil {
        fmt.Println("error:", err) // error: context deadline exceeded
        return
    }
    fmt.Println(result)
}

WithValue

Store request-scoped values (e.g., trace IDs, user info) in a context. Always use a private, unexported key type to avoid collisions:

package main
 
import (
    "context"
    "fmt"
)
 
type contextKey string
 
const requestIDKey contextKey = "requestID"
 
func WithRequestID(ctx context.Context, id string) context.Context {
    return context.WithValue(ctx, requestIDKey, id)
}
 
func RequestID(ctx context.Context) (string, bool) {
    id, ok := ctx.Value(requestIDKey).(string)
    return id, ok
}
 
func handle(ctx context.Context) {
    if id, ok := RequestID(ctx); ok {
        fmt.Println("handling request:", id)
    }
}
 
func main() {
    ctx := WithRequestID(context.Background(), "req-abc-123")
    handle(ctx)
}

Do not store optional parameters or configuration in context — only request-scoped data that crosses package boundaries.

Propagating Context to HTTP Requests

net/http integrates with context: every http.Request carries a context accessible via r.Context():

package main
 
import (
    "context"
    "fmt"
    "net/http"
    "time"
)
 
func fetchData(ctx context.Context, url string) error {
    req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
    if err != nil {
        return err
    }
 
    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
 
    fmt.Println("status:", resp.Status)
    return nil
}
 
func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()
 
    if err := fetchData(ctx, "https://go.dev"); err != nil {
        fmt.Println("error:", err)
    }
}

Key Takeaways