Understanding Go’s context Package: A Deep Dive

Go’s context package is one of the most important tools for building robust, cancellable, timeout-aware, concurrent programs. Whether you are writing HTTP …

Go’s context package is one of the most important tools for building robust, cancellable, timeout-aware, concurrent programs. Whether you are writing HTTP servers, gRPC services, background workers, or database operations, you will almost always use context.Context.

This article provides a deep, practical, and complete analysis of the context package using clear code examples.

1. Why context Exists

Modern Go programs are highly concurrent. You might start goroutines for:

  • database queries
  • API calls
  • background tasks
  • streaming events

But how do you cancel a goroutine?
How do you propagate deadlines across function calls?
How do you attach request-scoped values safely?

Go’s answer is:

Use context.Context for cancellation, timeouts, and request-scoped data.

2. What a Context Actually Is

context.Context is an interface, not a struct:

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

Every context supports:

  • Deadline(): When this context should expire
  • Done(): A channel closed when the context is cancelled
  • Err(): Why it was cancelled (context.Canceled or context.DeadlineExceeded)
  • Value(): Retrieve request-scoped data

3. The Root Context: context.Background()

Every context tree starts with either:

  • context.Background()
  • context.TODO()

Example:

ctx := context.Background()

Use cases:

  • top-level init
  • main function
  • tests

context.TODO() is used when you know a context is required but haven’t decided how to handle it yet.

4. Creating Cancellable Contexts

The most common pattern is:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

Full example:

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("worker stopped:", ctx.Err())
            return
        default:
            fmt.Println("working...")
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    go worker(ctx)

    time.Sleep(2 * time.Second)
    fmt.Println("Stopping worker")
    cancel()

    time.Sleep(500 * time.Millisecond)
}

Output:

working...
working...
...
Stopping worker
worker stopped: context canceled

5. Context With Timeout

A context automatically cancelled after a time.

ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()

Example:

package main

import (
    "context"
    "fmt"
    "time"
)

func longTask(ctx context.Context) error {
    select {
    case <-time.After(5 * time.Second):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    err := longTask(ctx)
    fmt.Println("Result:", err)
}

Output:

Result: context deadline exceeded

6. Context With Deadline

Similar to timeout, but with an absolute timestamp:

deadline := time.Now().Add(1 * time.Second)
ctx, cancel := context.WithDeadline(context.Background(), deadline)
defer cancel()

This is useful when propagating deadlines across service boundaries.

7. Passing Context Through Function Calls

A core rule of Go:

Always pass context as the first argument: func(ctx context.Context, ...)

Example:

func fetchData(ctx context.Context) (string, error) {
    select {
    case <-ctx.Done():
        return "", ctx.Err()
    case <-time.After(1 * time.Second):
        return "hello", nil
    }
}

func handler(ctx context.Context) error {
    data, err := fetchData(ctx)
    if err != nil {
        return err
    }
    fmt.Println("Data:", data)
    return nil
}

Never store a context inside a struct.
Never pass nil context.

8. Using Done() Correctly

Done() returns a channel that’s closed when the context is cancelled.

Wrong:

<-ctx.Done() // blocks forever if no cancel/timeout

Right:

select {
case <-ctx.Done():
    return ctx.Err()
case result := <-work:
    return result
}

Never ignore ctx.Err().

9. Context Values: When to Use (and Avoid)

Add values:

ctx := context.WithValue(context.Background(), "userID", 123)

Retrieve:

id := ctx.Value("userID")

But Go has strong guidance:

Context is for request-scoped data, NOT configuration.
Do not put everything into context.
Do not use string keys.

Example of proper key usage:

package main

import (
    "context"
    "fmt"
)

type userKey struct{} // empty struct as namespace

func main() {
    ctx := context.WithValue(context.Background(), userKey{}, "alice")

    name := ctx.Value(userKey{})
    fmt.Println(name)
}

This avoids collisions between different packages.

10. Cancellation Propagation

Cancelling a parent context cancels all children.

Example:

ctx, cancel := context.WithCancel(context.Background())
child1, _ := context.WithCancel(ctx)
child2, _ := context.WithTimeout(ctx, 5*time.Second)

cancel() // cancels parent

<-child1.Done() // immediately cancelled
<-child2.Done() // immediately cancelled

This is extremely useful in request/response pipelines.

11. Real-World Example: HTTP Server with Request Timeout

Example using Go’s http.Server (which automatically passes context):

package main

import (
    "context"
    "encoding/json"
    "net/http"
    "time"
)

type Response struct {
    Message string `json:"message"`
}

func slowOperation(ctx context.Context) (string, error) {
    select {
    case <-time.After(3 * time.Second):
        return "completed", nil
    case <-ctx.Done():
        return "", ctx.Err()
    }
}

func handler(w http.ResponseWriter, r *http.Request) {
    ctx, cancel := context.WithTimeout(r.Context(), 1*time.Second)
    defer cancel()

    res, err := slowOperation(ctx)
    if err != nil {
        http.Error(w, err.Error(), http.StatusGatewayTimeout)
        return
    }

    _ = json.NewEncoder(w).Encode(Response{Message: res})
}

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

Try accessing:

curl localhost:8080

Outputs:

context deadline exceeded

12. Using Context for Database Operations

Most DB drivers support context cancellation.

Example with database/sql:

ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
defer cancel()

row := db.QueryRowContext(ctx, "SELECT sleep(1)")
err := row.Scan(&data)

If the DB takes too long, cancel stops the query and returns an error.

13. Context in gRPC

gRPC heavily relies on context:

  • deadlines
  • metadata
  • cancellation

Example:

func (s *Server) GetUser(ctx context.Context, req *UserRequest) (*UserReply, error) {
    select {
    case <-ctx.Done():
        return nil, ctx.Err()
    default:
    }

    userID := req.Id
    // ...
    return &UserReply{Id: userID, Name: "Alice"}, nil
}

14. Best Practices (Must Follow)

Always pass context as the first parameter

func Process(ctx context.Context, input string)

Always honor context cancellation

Do not ignore <-ctx.Done().

Use context.WithValue sparingly

Only for request-scoped data:

  • request ID
  • user ID
  • trace ID

Avoid storing context inside structs

Bad:

type Service struct{ ctx context.Context }

Do not pass nil contexts

If you don’t know which to use:

ctx := context.TODO()

Wrap functions that may block in select {} with context

Correct:

select {
case result := <-ch:
case <-ctx.Done():
}

15. Summary

context is a foundational part of Go’s concurrency model.
It solves three important problems:

  1. Cancellation propagation
  2. Timeouts and deadlines
  3. Request-scoped values

You now know:

  • How to create and cancel contexts
  • How to use timeouts and deadlines
  • How to pass context across function calls
  • How to use and avoid context.WithValue
  • How context integrates with HTTP and gRPC

Keep Reading

Follow the engineering thread

Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.

Join newsletter Browse articles