Context — Cancellation and Deadlines
AbortController vs context.Context
Section titled “AbortController vs context.Context”In the browser/Node.js you use AbortController to cancel a fetch or any operation that accepts an AbortSignal:
const controller = new AbortController();setTimeout(() => controller.abort(), 5000); // cancel after 5 sconst res = await fetch(url, { signal: controller.signal });In Go, context.Context plays the same role — and goes further. It is a standard interface that carries a cancellation signal, an optional deadline, and arbitrary request-scoped values. You pass it as the first argument to every I/O function in the call chain so that a cancellation at the top automatically propagates all the way down.
context.WithCancel
Section titled “context.WithCancel”context.WithCancel returns a new context and a cancel function. Calling cancel closes the ctx.Done() channel, which every goroutine listening on that context will see:
// TypeScript: AbortControllerconst controller = new AbortController();const { signal } = controller;
async function worker(signal: AbortSignal) { // signal.aborted becomes true when abort() is called while (!signal.aborted) { await doWork(); } console.log("Worker cancelled");}
controller.abort(); // broadcast cancellation// Go: context.WithCancelpackage main
import ( "context" "fmt" "sync")
func worker(ctx context.Context, id int, wg *sync.WaitGroup) { defer wg.Done() select { case <-ctx.Done(): // unblocks when cancel() is called fmt.Printf("Worker %d cancelled: %v\n", id, ctx.Err()) }}
func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) go worker(ctx, i, &wg) }
cancel() // broadcast cancellation to all workers wg.Wait() fmt.Println("All workers stopped")}context.WithTimeout
Section titled “context.WithTimeout”For operations that must complete within a deadline, use context.WithTimeout. Go automatically cancels the context when the timer expires:
// TypeScript: timeout via Promise.raceasync function withTimeout<T>( fn: () => Promise<T>, ms: number): Promise<T> { const timeout = new Promise<never>((_, reject) => setTimeout(() => reject(new Error(`Timed out after ${ms}ms`)), ms) ); return Promise.race([fn(), timeout]);}// Go: context.WithTimeoutpackage main
import ( "context" "fmt" "sync" "time")
func slowOperation(ctx context.Context, wg *sync.WaitGroup) { defer wg.Done() select { case <-time.After(5 * time.Second): // simulated slow work fmt.Println("Operation complete") case <-ctx.Done(): fmt.Println("Operation timed out:", ctx.Err()) }}
func main() { ctx, cancel := context.WithTimeout( context.Background(), 100*time.Millisecond, // deadline: 100 ms ) defer cancel() // always defer cancel — prevents resource leak
var wg sync.WaitGroup wg.Add(1) go slowOperation(ctx, &wg) wg.Wait()}context.WithValue — request-scoped data
Section titled “context.WithValue — request-scoped data”// TypeScript: pass request-scoped data via function argumentsasync function handler(req: Request) { const userId = req.headers.get('x-user-id'); await processRequest(userId);}// Go: context.WithValue carries data through the call chainpackage main
import ( "context" "fmt")
type contextKey string
const userIDKey contextKey = "userID"
func processRequest(ctx context.Context) { userID := ctx.Value(userIDKey) fmt.Println("Processing for user:", userID)}
func main() { ctx := context.WithValue( context.Background(), userIDKey, "user-123", ) processRequest(ctx)}package main
import ( "context" "fmt" "sync")
func worker(ctx context.Context, id int, wg *sync.WaitGroup) { defer wg.Done() select { case <-ctx.Done(): fmt.Printf("Worker %d stopped: %v\n", id, ctx.Err()) }}
func main() { ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup
for i := 1; i <= 3; i++ { wg.Add(1) go worker(ctx, i, &wg) }
cancel() wg.Wait() fmt.Println("All workers stopped")}Loading Go runtime (first run only, ~8 MB)…