>_
GolangStepByStep
Software Engineer

Context in Production

Deadlines, request-scoped values, cancellation — do's and don'ts

# What is Context in Go? (The Analogy)

Imagine you order a complex meal at a busy restaurant. The waiter gives the order to the kitchen. Then you get an emergency call and have to leave immediately. You tell the waiter: "Cancel my order!"

The waiter runs to the kitchen and says "Stop cooking meal #42!" If they didn't do this, the kitchen would waste ingredients, time, and oven space making a meal for someone who isn't even in the building anymore.

In Go, context.Context is that Waiter.

  • Cancellation: It tells long-running database queries or HTTP calls to stop right now.
  • Timeouts: It acts as a timer ("If the pizza takes more than 30 mins, stop.").
  • Values: It carries specific instructions along the journey (like passing the "Order ID: #42", known as request-scoped data).

# Why is Context Required?

Without Context, a slow external API could block a goroutine forever. If 100 users try to do this, 100 goroutines get stuck. Soon, your server runs out of memory and crashes.

Context prevents Goroutine Leaks. It provides a universal, standardized way across the Go ecosystem to say:
"Hey, this request is dead, you can stop working on it now."

# Building Blocks: The Context Tree

Contexts are immutable, meaning you never modify one in place. Instead, you wrap a parent context to create a new child context.

1. The Root Contexts

Every tree needs a root. At the very top of your main() function or HTTP handler, you start with:

// The root. Never canceled. Has no values.
ctx := context.Background()

// When you are unsure what to use, but plan to fix it later.
ctxTodo := context.TODO()

2. Adding Timers with context.WithTimeout

If you want a call to fail naturally if it takes more than 5 seconds:

// parentCtx is our starting point
ctx, cancel := context.WithTimeout(parentCtx, 5*time.Second)

// VERY IMPORTANT: Always defer the cancel function!
// It frees up memory resources even if the function finishes early.
defer cancel() 

Rule of thumb: If a function takes a Context, it is assuming the caller will handle the timeout or cancellation. Pass ctx down to every database call (db.QueryContext(ctx, ...)) and HTTP request (http.NewRequestWithContext(ctx, ...)).

# Context Trees (Parent & Child Cancellation)

Contexts form a tree. If a parent is cancelled, all its children are cancelled automatically.

rootCtx := context.Background()

// Level 1: Canceled manually
authCtx, cancelAuth := context.WithCancel(rootCtx)

// Level 2: Child inherits from authCtx
dbCtx, cancelDB := context.WithTimeout(authCtx, 10*time.Second)
defer cancelDB()

// If we call cancelAuth(), BOTH authCtx and dbCtx are canceled instantly!
cancelAuth()

# context.WithValue (Request Scope Data)

Sometimes you want to pass invisible metadata down the call stack, such as Auth Tokens, Trace IDs, or User IDs.

DO NOT use context to pass required parameters, database connection pools, or config files. If a function needs a database, pass the database as a regular argument or struct field. Context is for invisible, transient data only!

// 1. Define a custom type for your key to avoid collisions
type requestIDKey string
const key = requestIDKey("reqID")

// 2. Store the value
ctx := context.WithValue(context.Background(), key, "1234-abcd")

// 3. Retrieve the value deeper in the code
if val := ctx.Value(key); val != nil {
    fmt.Println("Request ID is:", val.(string))
}

# Go Design Rules For Context

  • Always be the first parameter: func DoWork(ctx context.Context, data string)
  • Never store context in a struct: Do not add ctx as a field in a struct type. Pass it explicitly to methods.
  • Never pass a nil context: If you don't have one, use context.TODO().
  • Cancel functions: The code that creates a cancel/timeout context is the one responsible for calling cancel(). Always defer cancel() immediately.
practice & review