Context Complete

Updated

September 8, 2026

Context Complete

Overview

context.Context carries cancellation, deadlines, and optional request-scoped values. Prefer it over ad-hoc cancel channels for public APIs.

From cancel channel to context

// Channel style
func execute(cancel <-chan struct{}, fn func() int) (int, error) {
    ch := make(chan int, 1)
    go func() { ch <- fn() }()
    select {
    case res := <-ch:
        return res, nil
    case <-cancel:
        return 0, errors.New("canceled")
    }
}

// Context style
func execute(ctx context.Context, fn func() int) (int, error) {
    ch := make(chan int, 1)
    go func() { ch <- fn() }()
    select {
    case res := <-ch:
        return res, nil
    case <-ctx.Done():
        return 0, ctx.Err()
    }
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // always

Multiple cancel() calls are safe (unlike double close on a channel).

Context is a tree

  Background
     └── WithCancel / WithTimeout
            └── children inherit cancel downward
  shorter deadline always wins
Rule Detail
Immutable nodes “Add” property → new child
Parent cancel cancels all children
Child cancel does not cancel parent/siblings
Timeouts nest shorter deadline wins; child cannot extend parent
parent, cancel := context.WithTimeout(context.Background(), 200*time.Millisecond)
defer cancel()
child, cancel2 := context.WithTimeout(parent, 50*time.Millisecond)
defer cancel2()
// work with child: max wait is 50ms

Soft child timeout longer than parent is pointless—parent fires first.

Timeout and deadline

ctx, cancel := context.WithTimeout(parent, 150*time.Millisecond)
// equivalent core:
// context.WithDeadline(parent, time.Now().Add(150*time.Millisecond))
defer cancel()

// errors
// context.Canceled
// context.DeadlineExceeded

execute need not know why Done closed—only that it should stop waiting.

Values

type key int
const requestID key = 1
ctx = context.WithValue(ctx, requestID, "abc")
id, _ := ctx.Value(requestID).(string)

Use for request metadata (IDs, loggers)—not for optional parameters that belong in function args.

Propagate into work

Best practice: fn accepts ctx and checks ctx.Done() or passes ctx to I/O:

func work(ctx context.Context) (int, error) {
    select {
    case <-time.After(100 * time.Millisecond):
        return 42, nil
    case <-ctx.Done():
        return 0, ctx.Err()
    }
}

Otherwise timeout only abandons the waiter, not the worker.

Runnable example

go mod init example && go run .
package main

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

func execute(ctx context.Context, fn func() int) (int, error) {
    ch := make(chan int, 1)
    go func() { ch <- fn() }()
    select {
    case v := <-ch:
        return v, nil
    case <-ctx.Done():
        return 0, ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
    defer cancel()
    v, err := execute(ctx, func() int {
        time.Sleep(100 * time.Millisecond)
        return 42
    })
    fmt.Println(v, err)
}

Expected: 0 context deadline exceeded

Try next: Nest a 10ms child under a 1s parent and confirm the child wins.