Contexts: To Cancel or Not to Cancel
Harutyun Mardirossian

Harutyun Mardirossian @crusty0gphr

About: Go 🪐 | Beer-Driven-Development 🍺

Location:
Yerevan, Armenia
Joined:
Dec 21, 2017

Contexts: To Cancel or Not to Cancel

Publish Date: Jun 9
3 2

In Go, the context package provides a mechanism to propagate cancellation signals, deadlines, and request-scoped values across API boundaries and goroutines. It enables graceful termination of operations by allowing parent operations to cancel child operations. This prevents goroutine leaks and ensures resources are cleaned up efficiently when work is no longer needed.

To shut down a chain of goroutines using context in Go, you create a cancelable context that propagates cancellation signals through the chain. Each goroutine monitors ctx.Done() to exit cleanly when cancellation is requested.

context cancelation diagram

Each caller must be able to cancel its callee, use context derivation and hierarchical cancellation. Each function creates a cancellable context for its callee, enabling independent cancellation at any level.

func A(ctx context.Context) {
    // Create child context for B (cancels when A exits or manually cancelled)
    ctxA, cancelA := context.WithCancel(ctx)
    defer cancelA() // Cancel B when A exits
Enter fullscreen mode Exit fullscreen mode

...

func B(ctx context.Context) {
    // Create child context for C
    ctxB, cancelB := context.WithCancel(ctx)
    defer cancelB() // Cancel C when B exits
Enter fullscreen mode Exit fullscreen mode

Hierarchy Creation:

  • mainA with ctx
  • AB with ctxA (child of ctx)
  • BC with ctxB (child of ctxA)

The code below demonstrates hierarchical cancellation of goroutines using Go's context package.

package main

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

func A(ctx context.Context) {
    // Create child context for B (cancels when A exits or manually canceled)
    ctxA, cancelA := context.WithCancel(ctx)
    defer cancelA() // Cancel B when A exits

    go B(ctxA) // Start B (no need to track separately)

    // A's work
    for i := 0; ; i++ {
        select {
        case <-ctx.Done(): // Main cancelled us
            fmt.Println("A: shutdown signal received")
            return
        case <-time.After(500 * time.Millisecond):
            fmt.Println("A: working", i)

            // Example: A decides to cancel B after 3 iterations
            if i == 2 {
                fmt.Println("A: manually cancelling B")
                cancelA() // Cancel only B (not A itself)
            }
        }
    }
}

func B(ctx context.Context) {
    // Create child context for C
    ctxB, cancelB := context.WithCancel(ctx)
    defer cancelB() // Cancel C when B exits

    go C(ctxB) // Start C

    // B's work
    for i := 0; ; i++ {
        select {
        case <-ctx.Done(): // A cancelled us
            fmt.Println("B: shutdown signal received")
            return
        case <-time.After(500 * time.Millisecond):
            fmt.Println("B: working", i)
        }
    }
}

func C(ctx context.Context) {
    // C's work
    for i := 0; ; i++ {
        select {
        case <-ctx.Done(): // B cancelled us
            fmt.Println("C: shutdown signal received")
            return
        case <-time.After(500 * time.Millisecond):
            fmt.Println("C: working", i)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())
    go A(ctx) // Start chain

    // Let run for 5 seconds
    time.Sleep(5 * time.Second)
    fmt.Println("\nmain: cancelling entire chain")
    cancel() // Cancel everything

    // Allow time for shutdown messages
    time.Sleep(500 * time.Millisecond)
}
Enter fullscreen mode Exit fullscreen mode

A explicitly cancels B after 3 iterations using its cancel function (cancelA), which automatically propagates to C since C's context is derived from B's. Meanwhile, the main function cancels the entire chain after 5 seconds by calling its root cancel function, which propagates downward through all derived contexts.

Each function monitors its own context's Done() channel, allowing immediate termination when cancellation occurs. The defer cancel() statements ensure child contexts are always properly cancelled when parents exit, preventing goroutine leaks. The output shows work progress interleaved with shutdown messages, demonstrating both targeted cancellation (AB) and full-chain cancellation (mainAB→C).

Comments 2 total

Add comment