Skip to main content

Go Ep 12: Context, Cancellation & Timeouts

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
go - This article is part of a series.
Part 12: This Article
When an incoming HTTP request is cancelled by the client, every downstream database query and RPC call associated with that request must stop immediately. Go’s context.Context enforces this cancellation propagation.

TL;DR (Quick Summary)
#

  • context.Context: Standard interface carrying cancellation signals, deadlines, and request metadata.
  • Golden Rule: Always pass ctx context.Context as the first parameter of a function (func DoWork(ctx context.Context, ...)).
  • WithTimeout(parent, duration): Automatically sends a cancellation signal via ctx.Done() when the deadline expires.
  • WithCancel(parent): Manual cancellation trigger (cancel()).
  • WithValue(parent, key, val): Attaches immutable request-scoped metadata (e.g. Request ID, Auth Token).

1. Context Cancellation Tree Propagation
#


graph TD
    subgraph ContextTree ["Context Tree"]
        RootContext["context.Background()"] --> RequestCtx["context.WithTimeout(Root, 2s)"]
        RequestCtx --> ServiceCtx["Service Context"]
        ServiceCtx --> QueryCtx["Database Context"]
        ServiceCtx --> APICtx["HTTP Client Context"]
    end

    TimeoutSignal["2s Timeout Expired"] -->|Cancels| RequestCtx
    RequestCtx -->|Propagates Cancellation Downward| ServiceCtx
    ServiceCtx -->|Cancels Active Work| QueryCtx
    ServiceCtx -->|Cancels Active Work| APICtx

2. Step-by-Step Lab: Implementing HTTP Client & Database Timeouts
#

package main

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

// Simulate a slow database query
func queryDatabase(ctx context.Context) (string, error) {
 select {
 case <-time.After(500 * time.Millisecond): // Takes 500ms
  return "Query Result: User Record 101", nil
 case <-ctx.Done(): // Triggered if timeout expires early!
  return "", ctx.Err() // Returns context.DeadlineExceeded or Canceled
 }
}

func main() {
 // Scenario 1: Timeout shorter than query time (Triggers Cancellation)
 fmt.Println("=== Scenario 1: Short Timeout (100ms) ===")
 ctx1, cancel1 := context.WithTimeout(context.Background(), 100*time.Millisecond)
 defer cancel1()

 res1, err1 := queryDatabase(ctx1)
 if err1 != nil {
  fmt.Println("Operation Aborted:", err1) // context.DeadlineExceeded
 } else {
  fmt.Println("Success:", res1)
 }

 // Scenario 2: Timeout longer than query time (Succeeds)
 fmt.Println("\n=== Scenario 2: Sufficient Timeout (1s) ===")
 ctx2, cancel2 := context.WithTimeout(context.Background(), 1*time.Second)
 defer cancel2()

 res2, err2 := queryDatabase(ctx2)
 if err2 != nil {
  fmt.Println("Operation Aborted:", err2)
 } else {
  fmt.Println("Success:", res2)
 }
}

Expected Terminal Output:

=== Scenario 1: Short Timeout (100ms) ===
Operation Aborted: context deadline exceeded

=== Scenario 2: Sufficient Timeout (1s) ===
Success: Query Result: User Record 101

3. Comparison: Context Constructors
#

ConstructorBehaviorWhen to Use
context.Background()Non-nil, empty root context.Top-level entry points (main(), request handlers).
WithTimeout(parent, duration)Cancels automatically when duration elapses.Database queries, external HTTP calls.
WithCancel(parent)Cancels manually when cancel() is called.Aborting background worker Goroutines.
WithValue(parent, key, val)Stores immutable key-value metadata.Passing Request IDs, Trace IDs, or Auth Claims.

4. Troubleshooting & Common Errors
#

Error 1: Forgetting to Call cancel()
#

The Cause: WithTimeout or WithCancel allocates resources in the context tree. Failing to call cancel() leaks memory and timers until the parent context expires! The Fix: Always defer cancel() immediately after creating a context:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // 🟢 Guarantees cleanup when function exits!

Summary & Next Steps
#

In this episode:

  • We passed context.Context across function boundaries.
  • We set strict execution timeouts using context.WithTimeout.
  • We observed downward cancellation propagation.

In Episode 13: Generics in Go, we will master Type Parameters [T any] and type constraints!

go - This article is part of a series.
Part 12: This Article