The Most Misunderstood 73 Lines in Go

go dev.to

The first time I saw context.Context, I thought:

"Oh, a way to pass timeouts. Nice."

I was wrong.

Embarrassingly wrong.

The kind of wrong that quietly leaks goroutines for 18 months before someone notices.

Then I thought:

"Okay, it's for cancellation."

Still wrong.

Closer.

But wrong.

The truth is stranger—and far more powerful.

context.Context is:

  • The most elegant solution to a problem you didn't know you had
  • The most common source of bugs you don't realize you're writing
  • The backbone of modern Go concurrency

Let's fix that.

Forever.


Part 1: The Lie They Tell You About Context

Most tutorials teach Context like this:

"Context is a way to set deadlines and cancel operations."

Example:

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

result, err := doSomething(ctx)
Enter fullscreen mode Exit fullscreen mode

✅ Correct

✅ Useful

❌ Only 5% of the story


The Real Definition

Context is an immutable value that flows through your call graph, carrying cancellation signals, deadlines, and request-scoped data from root to leaf.

It never flows backward.

It never flows sideways.

Read that twice.

Everything else follows from this.


Part 2: The One Rule That Explains Everything

Context flows DOWN.

Never up.

Never sideways.

Request
   │
   ▼
Handler
   │
   ▼
Service
   │
   ▼
Repository
   │
   ▼
Database
Enter fullscreen mode Exit fullscreen mode

The same context travels through the entire operation:

ctx ──────────────────────────────►
Enter fullscreen mode Exit fullscreen mode

Creating Child Contexts

parent := ctx

child, cancel :=
    context.WithCancel(parent)

grandchild, _ :=
    context.WithTimeout(child, time.Second)
Enter fullscreen mode Exit fullscreen mode

Perfectly legal.


What You Cannot Do

parent := context.WithCancel(child)
Enter fullscreen mode Exit fullscreen mode

❌ Doesn't exist.

merged := context.Merge(ctx1, ctx2)
Enter fullscreen mode Exit fullscreen mode

❌ Also doesn't exist.


Why?

Because a context represents:

  • One request
  • One operation
  • One unit of work

When a parent dies:

Parent
 ├── Child A
 ├── Child B
 └── Child C
Enter fullscreen mode Exit fullscreen mode

Everything below it dies too.

But killing a child never affects the parent.

That predictability is the entire point.


Part 3: The Four Context Superpowers


⚡ Superpower 1 — Cancellation

The one everyone knows.

func SearchProducts(
    ctx context.Context,
    query string,
) ([]Product, error) {

    searchCtx, cancel :=
        context.WithTimeout(ctx, time.Second)

    defer cancel()

    return db.Query(
        searchCtx,
        "SELECT * FROM products ...",
    )
}
Enter fullscreen mode Exit fullscreen mode

When the timeout expires:

  • Database receives cancellation
  • Query stops
  • Resources get cleaned up

No zombies.

No hanging connections.


The Secret Nobody Mentions

Cancellation is cooperative.

Context doesn't magically stop your code.

Your code must cooperate.

Bad

func SlowOperation(ctx context.Context) error {
    for i := 0; i < 1_000_000; i++ {
        doExpensiveThing()
    }

    select {
    case <-ctx.Done():
        return ctx.Err()
    default:
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

This ignores cancellation until the end.


Good

func SlowOperation(ctx context.Context) error {
    for i := 0; i < 1_000_000; i++ {

        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
        }

        doExpensiveThing()
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Check often.

Exit early.


⚡ Superpower 2 — Value Propagation

Context can carry request-scoped metadata.

ctx = context.WithValue(
    ctx,
    userIDKey,
    "alice-123",
)
Enter fullscreen mode Exit fullscreen mode

Later:

userID := ctx.Value(userIDKey)
Enter fullscreen mode Exit fullscreen mode

Danger Zone

This compiles:

userID := ctx.Value(userIDKey).(string)
Enter fullscreen mode Exit fullscreen mode

And this also compiles:

userID := ctx.Value(userIDKey).(int)
Enter fullscreen mode Exit fullscreen mode

The second one explodes at runtime.


Use Context Values For...

✅ Good

  • Request IDs
  • User IDs
  • Tenant IDs
  • Tracing
  • Logging metadata

❌ Bad

  • Database connections
  • Service dependencies
  • Business logic inputs
  • Optional configuration

Golden Rule

If business logic requires it, pass it explicitly.

If it's plumbing, context is acceptable.


⚡ Superpower 3 — Deadline Propagation

Parent deadlines always win.

ctx, cancel :=
    context.WithTimeout(
        context.Background(),
        5*time.Second,
    )
defer cancel()
Enter fullscreen mode Exit fullscreen mode

Now create a child:

childCtx, childCancel :=
    context.WithTimeout(
        ctx,
        10*time.Second,
    )
defer childCancel()
Enter fullscreen mode Exit fullscreen mode

Guess the deadline?

Not 10 seconds.

Still 5.


Rule

Context deadlines are minimal.

The strictest deadline wins.

Always.

This prevents downstream code from extending upstream limits.


⚡ Superpower 4 — Goroutine Lifecycle Management

This is where senior engineers separate themselves.


Bad

func startWorker() {
    go func() {
        for {
            time.Sleep(time.Second)
            doWork()
        }
    }()
}
Enter fullscreen mode Exit fullscreen mode

This worker lives forever.

Or until production starts smoking.


Good

func startWorker(ctx context.Context) {
    go func() {

        ticker := time.NewTicker(time.Second)
        defer ticker.Stop()

        for {
            select {

            case <-ctx.Done():
                return

            case <-ticker.C:
                doWork()
            }
        }
    }()
}
Enter fullscreen mode Exit fullscreen mode

Usage:

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

startWorker(ctx)

cancel()
Enter fullscreen mode Exit fullscreen mode

Worker exits gracefully.


Rule

Every goroutine without a context is a potential memory leak.

Every.

Single.

One.


Part 4: The Context Crimes


☠️ Crime 1 — Storing Context in Structs

type Repository struct {
    ctx context.Context
    db  *sql.DB
}
Enter fullscreen mode Exit fullscreen mode

Never do this.

Why?

  • Context becomes stale
  • Request ownership becomes unclear
  • Testing becomes painful

Correct

type Repository struct {
    db *sql.DB
}

func (r *Repository) GetUser(
    ctx context.Context,
    id int,
) (*User, error) {
    ...
}
Enter fullscreen mode Exit fullscreen mode

☠️ Crime 2 — Ignoring Cancellation

Bad:

http.Get(url)
Enter fullscreen mode Exit fullscreen mode

This uses a background context.

Your timeout is ignored.


Good:

req, _ :=
    http.NewRequestWithContext(
        ctx,
        "GET",
        url,
        nil,
    )

resp, err :=
    http.DefaultClient.Do(req)
Enter fullscreen mode Exit fullscreen mode

Now cancellation actually works.


☠️ Crime 3 — Background Work Using Request Context

go func() {
    expensiveOperation(r.Context())
}()
Enter fullscreen mode Exit fullscreen mode

Looks fine.

It's not.

Request ends.

Context cancels.

Background work dies.


Fix

bgCtx := context.WithoutCancel(
    r.Context(),
)
Enter fullscreen mode Exit fullscreen mode

Now it survives the request.


☠️ Crime 4 — Nil Contexts

if ctx == nil {
    ctx = context.Background()
}
Enter fullscreen mode Exit fullscreen mode

Don't.

A nil context is a caller bug.

Make it fail loudly.


Part 5: Patterns You'll Actually Use


🥪 The Timeout Sandwich

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

defer cancel()
Enter fullscreen mode Exit fullscreen mode

Create short-lived child contexts around external dependencies.

Always.


🆔 Request ID Propagation

Middleware:

ctx := WithRequestID(
    r.Context(),
    requestID,
)
Enter fullscreen mode Exit fullscreen mode

Handler:

log.Printf(
    "[%s] Processing request",
    GetRequestID(ctx),
)
Enter fullscreen mode Exit fullscreen mode

Instant traceability.


🔀 Fan-Out With Early Cancellation

Launch multiple queries:

search
recommendation
inventory
Enter fullscreen mode Exit fullscreen mode

Collect results.

Exit immediately on failure.

All goroutines respect the same context.

One cancellation signal controls everything.

Beautiful.


Part 6: The Context Decision Tree

Do you already have a context?
        │
   ┌────┴────┐
   │         │
  YES       NO
   │         │
 Pass it     ▼
 down     Background job?
               │
        ┌──────┴──────┐
        │             │
       YES           NO
        │             │
Background()      TODO()
Enter fullscreen mode Exit fullscreen mode

The Rule That Prevents Leaks

Every one of these:

context.WithCancel(...)
context.WithTimeout(...)
context.WithDeadline(...)
Enter fullscreen mode Exit fullscreen mode

Must have:

defer cancel()
Enter fullscreen mode Exit fullscreen mode

Immediately.

Not later.

Immediately.


🔍 This Week's Challenge

Audit your codebase.

Find:

  • [ ] Goroutines without contexts
  • [ ] Context stored in structs
  • [ ] Raw http.Get()
  • [ ] Missing defer cancel()
  • [ ] Loops ignoring ctx.Done()

You'll find bugs.

Guaranteed.


The Mental Model

Remember only this:

Context flows down.
Cancellation flows up.
Values are for plumbing.
Business data is explicit.
Enter fullscreen mode Exit fullscreen mode

Everything else is implementation detail.


One-Page Cheat Sheet

✅ Do This

func DoWork(
    ctx context.Context,
    arg string,
) error {
    return doSubWork(ctx, arg)
}
Enter fullscreen mode Exit fullscreen mode
ctx, cancel :=
    context.WithTimeout(
        parent,
        5*time.Second,
    )

defer cancel()
Enter fullscreen mode Exit fullscreen mode
func handler(
    w http.ResponseWriter,
    r *http.Request,
) {
    ctx := r.Context()
}
Enter fullscreen mode Exit fullscreen mode

❌ Don't Do This

type Worker struct {
    ctx context.Context
}
Enter fullscreen mode Exit fullscreen mode
ctx, cancel :=
    context.WithTimeout(
        parent,
        5*time.Second,
    )

// forgot cancel()
Enter fullscreen mode Exit fullscreen mode
http.Get(url)
Enter fullscreen mode Exit fullscreen mode

Final Thought

Most developers think Context is about timeouts.

It's not.

Timeouts are merely a feature.

The real purpose of context.Context is to give an entire tree of operations a shared lifecycle.

Once that clicks:

  • Timeouts make sense
  • Cancellation makes sense
  • Request scoping makes sense
  • Goroutine management makes sense

And suddenly those mysterious 73 lines in the standard library become one of the most elegant designs in Go.

Context flows down.

Never forget it.

Source: dev.to

arrow_back Back to Tutorials