skip to content

The context Package

How a Go program says "stop, the caller is gone": one context.Context threaded through every call, carrying cancellation, deadlines, and request-scoped values. Interviewers treat correct context propagation as the marker of real production Go experience.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

explore

questions

20

What is context.Context in Go, and why is it passed as a function's first parameter?

level: juniorimportance: must knowfreq 85%

answer

  1. a caller that gave up must say so
  2. Go has no goroutine-local storage
  3. one value threaded through every call
  4. cancellation, a deadline, request-scoped values
  5. conventionally first, conventionally named ctx

basics

~20 s

context.Context carries a cancellation signal, an optional deadline and request-scoped values across API boundaries. Go has no goroutine-local storage, so it is threaded explicitly through every call, first in the parameter list and named ctx.

solid answer

~40 s

`context.Context` is an interface that carries three things down a call chain: a signal that the caller has given up (`ctx.Done()`), an optional deadline (`ctx.Deadline()`), and request-scoped values (`ctx.Value`). Go has no goroutine-local storage, so there is nowhere implicit to put that signal — it has to be an ordinary parameter, and the convention is that it comes first and is named `ctx`: `func Fetch(ctx context.Context, id string) (*Item, error)`. Cancellation is cooperative: cancelling does not kill anything, it closes the `Done` channel, and every function in the chain is responsible for noticing and returning early. That is why the value must reach every layer — a function that drops it silently becomes uncancellable, and every deadline the caller set stops applying below that point.

code

go · 8 lines
go
func (s *Store) Order(ctx context.Context, id string) (*Order, error) {
	row := s.db.QueryRowContext(ctx, "SELECT total FROM orders WHERE id = $1", id)
	var o Order
	if err := row.Scan(&o.Total); err != nil {
		return nil, err
	}
	return &o, nil
}

go deeper

for a junior

Be ready to say in one sentence what a context.Context carries and where it goes in a signature: first parameter, named ctx. Know that passing nil is never right and that cancellation is a signal, not a kill.

for a middle

Expect to name the four methods and explain that a context is immutable, so you derive children rather than mutating one. Be able to explain why cancellation must be cooperative in Go: there is no way to stop a goroutine from outside.

for a senior

Show that you treat a missing ctx parameter as a defect in review, because one un-plumbed helper makes every deadline above it unenforceable. Be ready to describe how you would find such gaps across an existing codebase.

for a principal

Own the convention itself: whether every exported call in your org's shared libraries must take a ctx, what you do about the ones that legitimately never block, and how much cost you accept to make the rule uniform and machine-checkable rather than case-by-case.

## The problem it solves A server handles a request. Halfway through, the client hangs up, or the request has already burned its two-second budget. Everything still running on that request's behalf — a database query, an outbound HTTP call, a retry loop, three goroutines fanned out over shards — is now wasted work holding memory, a connection and a scheduler slot. Something has to tell all of it to stop. Many runtimes answer this with thread-local storage or a special cancellable task object. Go has neither: there is no goroutine-local storage, and a goroutine is not an object you hold a handle to. So Go answers it with an ordinary value that you pass along: `context.Context`. ## What the value actually is `context.Context` is an interface with four methods: ```go type Context interface { Deadline() (deadline time.Time, ok bool) Done() <-chan struct{} Err() error Value(key any) any } ``` A callee that receives one can ask three useful questions and read one piece of request-scoped data: - **"Should I stop?"** — `Done()` returns a receive-only channel that is *closed* when the work should be abandoned. Closing is the signal; a closed channel makes every receive succeed immediately, so any number of goroutines can watch the same one. - **"Why did I stop?"** — `Err()` returns `nil` while the work is still live, and a non-nil error once `Done` is closed. - **"How long do I have?"** — `Deadline()` returns a time and a boolean saying whether a deadline was set at all. - **"What request is this?"** — `Value(key)` looks up request-scoped data such as a request or trace identifier. A `Context` is **immutable**. Nothing you can call on it changes it. You do not "set a deadline on a context"; you *derive a new context* from it with one of the `context.With…` functions, and pass the child down. That immutability is what makes it safe to hand the same `ctx` to several goroutines at once with no locking. ## Why it is the first parameter The convention — stated in the `context` package documentation itself — is that a function taking a context takes it as its **first** parameter, conventionally named `ctx`: ```go func (s *Store) Order(ctx context.Context, id string) (*Order, error) ``` This is a pure convention; the compiler does not care. It exists because it is *mechanically checkable*. A reviewer scanning a diff, or a lint rule in CI, can see at a glance whether every exported call in a package is cancellable, because a cancellable one always looks the same. Put it third, or hide it in a struct field, and "is this call cancellable?" becomes a question you have to answer by reading the body. The matching rule is that you **never pass `nil`** as a context, even to a function that would tolerate it. If you genuinely have no context to hand — you are at the top of `main`, in a test, or in code that has not been plumbed yet — pass an explicit empty root instead. ## Cooperative, not forcible The single most common misunderstanding is that cancelling a context *stops* something. It does not. Go cannot kill a goroutine from the outside; there is no `goroutine.Kill()`. All cancellation does is close a channel. Every function in the chain has to opt in to noticing: ```go func work(ctx context.Context, items []string) error { for _, it := range items { select { case <-ctx.Done(): return ctx.Err() default: } process(it) } return nil } ``` In practice most functions never write that loop, because they simply pass `ctx` on to something that already honours it — a database call, an outbound HTTP request — and the check happens down there. That is exactly why *propagation* is the whole game. The chain is only as cancellable as its least diligent link: one helper that takes no context, or that quietly starts a fresh empty root of its own, severs the signal for everything below it, and the deadline the caller set no longer means anything. ## What it is not It is not a general-purpose argument bag. Anything the callee needs in order to do its job — a database handle, a logger, configuration, an ID — is a parameter or a field on the receiver. The context carries the *request's* cross-cutting concerns: when to stop, and the few identifiers that describe which request this is. It is also not a scheduling primitive. It starts nothing, joins nothing and waits for nothing. It is a one-way broadcast that says "whoever is working on my behalf may stop now."

  • Is it safe to pass one context.Context to several goroutines running at the same time?
    Yes. A `Context` is immutable and its documentation states it is safe for simultaneous use by multiple goroutines. Cancellation works by *closing* the `Done` channel, and a closed channel unblocks every receiver, so a hundred goroutines can select on the same `ctx.Done()` with no lock. You never mutate a context; you derive a child from it.
  • If Go had goroutine-local storage, would context.Context still need to be a parameter?
    Probably not in the same form, and that trade is deliberate. An implicit ambient context is invisible in a signature: you cannot tell whether a call is cancellable without reading it, and a value silently leaks across an unrelated boundary. Making it a parameter costs a word of boilerplate per function and buys a cancellability contract you can see, review and lint.
  • Where does the error parameter go relative to ctx in a Go signature?
    They are at opposite ends: `ctx context.Context` is the first parameter, and `error` is the last return value. So the canonical exported shape is `func Do(ctx context.Context, args…) (Result, error)`. Both conventions exist so a reader can classify a signature without reading the body.

It is the note a caller pins to a job before handing it down the line: "here is who this is for, here is when it stops being worth doing." Everyone touching the job can read the note, and nobody is forced to obey it — but the ones who don't read it keep working long after the customer has left.

saying these in an interview costs you the question

  • Says cancelling a context kills the goroutine that received it
  • Puts ctx last in the parameter list or hides it on the receiver
  • Passes nil where a context.Context is required
  • Treats context.Context as a general-purpose bag of arguments
  • Thinks a deadline on a context enforces itself without anyone checking
open as a page

What does context.WithCancel return, and why must the cancel function always be called?

level: juniorimportance: must knowfreq 78%

basics

~20 s

context.WithCancel(parent) returns a derived Context plus a cancel function. Calling cancel closes that context's Done channel and detaches it from its parent. Skipping the call leaves the child attached to a long-lived parent, so its memory is retained.

open as a page

In Go, what is the difference between context.WithTimeout and context.WithDeadline?

level: juniorimportance: must knowfreq 72%

basics

~10 s

context.WithTimeout takes a duration and expires that far from now; context.WithDeadline takes an absolute time.Time and expires at that instant. WithTimeout is defined as WithDeadline with time.Now() plus the duration.

open as a page

Which Go standard-library calls actually stop early when the context.Context you passed them is cancelled?

level: juniorimportance: must knowfreq 72%

basics

~10 s

Only the calls you handed the context to. database/sql's QueryContext, http.NewRequestWithContext, net.Dialer.DialContext and exec.CommandContext abort. Anything with no ctx parameter, such as os.File.Read, io.Copy or time.Sleep, runs to completion.

open as a page

Why should a context.WithValue key be an unexported custom type rather than a plain string?

level: juniorimportance: must knowfreq 62%

basics

~20 s

Context values share one namespace across every package in a program. A key of an unexported type cannot be constructed or matched by any other package, so two libraries that both use the string "userID" cannot overwrite each other's value.

open as a page

What do a context.Context's Done, Err and Deadline methods tell the function that received it?

level: middleimportance: must knowfreq 62%

basics

~20 s

Done returns a channel closed when the work should stop; Err is nil until then and afterwards says why; Deadline returns when the work must finish plus a boolean saying whether one was set at all.

open as a page

In a worker loop, how do you observe cancellation of a context.Context, and what do you return?

level: middleimportance: must knowfreq 70%

basics

~20 s

Put a case <-ctx.Done() in the select alongside the loop's other channel operations, and return ctx.Err(), which is context.Canceled once cancel has been called. Cancellation is advisory: code that never looks at Done keeps running.

open as a page

Your handler sets a 2s context.WithTimeout, but its outbound HTTP and database calls still run for 30s. Why?

level: seniorimportance: must knowfreq 52%

basics

~20 s

Almost certainly the context was derived and then never passed to the calls. A Go deadline enforces nothing by itself; it only bites when the ctx reaches the operation, via http.NewRequestWithContext, QueryContext and the other ctx-taking variants.

open as a page

How do context.Background and context.TODO differ, and how is a context tree built from a root?

level: middleimportance: should knowfreq 58%

basics

~20 s

context.Background and context.TODO behave identically: both are empty roots, never cancelled, with no deadline and no values. TODO only marks unfinished plumbing. Every other context is derived from a root, forming a tree that cancels downward.

open as a page

What does context.WithTimeout(ctx, 5*time.Second) return when ctx already has 200ms left?

level: middleimportance: should knowfreq 58%

basics

~10 s

A context that still expires in about 200 milliseconds. A derived context can only shorten its parent's deadline, never extend it, so the earliest deadline in the chain wins and Err() then reports context.DeadlineExceeded.

open as a page

Why doesn't a blocked os.File.Read return when the context.Context governing the job is cancelled, and what do you do instead?

level: middleimportance: should knowfreq 50%

basics

~20 s

os.File.Read has no context parameter and the read is already in the kernel, so it returns only on data, EOF or error. Cancel between reads instead: read in bounded chunks and check ctx.Err() on each iteration.

open as a page

How does ctx.Value locate a value, and what does that cost as the context chain grows?

level: middleimportance: should knowfreq 45%

basics

~20 s

Each context.WithValue call wraps the parent in a new node holding one key and value. Value checks its own key, then asks its parent — so a lookup is a linear walk up the chain, and a miss reaches the root.

open as a page

A client SDK stores the context.Context from NewClient in a struct field. What breaks?

level: seniorimportance: should knowfreq 42%

basics

~20 s

Every call then runs under the context that existed at construction, not the caller's. A stored request context poisons the client once that request ends; a stored empty root ignores every caller's deadline. Pass ctx per method instead.

open as a page

In a transcoding job runner, context.WithCancel is called per job but cancel runs only on the error path. What leaks, and how do you find every such site?

level: seniorimportance: should knowfreq 46%

basics

~20 s

Each successful job leaves its derived context registered in the long-lived parent's children set, so the parent retains every child. Memory creeps in step with throughput. Sweep the module with go vet, whose lostcancel check flags exactly this.

open as a page

Your batch job's context.Context is cancelled and its cleanup writes now fail instantly. How do you make cleanup still run?

level: seniorimportance: should knowfreq 44%

basics

~20 s

Do not run cleanup on the cancelled context. Derive one with context.WithoutCancel, which keeps the values but is never cancelled, bound it yourself, and use that for the closing writes. Files still need your own defer Close.

open as a page

A handler panics dereferencing what ctx.Value returned because no middleware ran on that path — how do you make it safe?

level: seniorimportance: should knowfreq 40%

basics

~20 s

ctx.Value returns a nil interface when nothing stored the key, so a bare assertion panics and a comma-ok assertion yields a nil pointer. Give every key one accessor returning a value plus an ok flag, and test the absent path.

open as a page

With context.WithCancelCause, what do ctx.Err() and context.Cause(ctx) each return after cancellation?

level: middleimportance: nice to knowfreq 32%

basics

~10 s

ctx.Err() still returns context.Canceled, the generic class of ending. context.Cause(ctx) returns the specific error handed to the cancel func. Before any cancellation, Cause returns nil; if cancel was called with nil, Cause matches ctx.Err().

open as a page

When the context.Context passed to exec.CommandContext is cancelled, what happens to the child process, and why can Wait still hang?

level: seniorimportance: nice to knowfreq 32%

basics

~20 s

By default the child is killed outright: exec.CommandContext installs a Cancel func that calls Process.Kill, which is SIGKILL and ungraceful. Wait can still hang afterwards, because it also waits for the goroutines copying the inherited pipes.

open as a page

How do you decide whether a shared Go client package sets its own context.WithTimeout or only honours the caller's deadline?

level: principalimportance: nice to knowfreq 32%

basics

~10 s

Treat the caller's deadline as authoritative and any package-set timeout as a configurable cap that mainly applies when the caller supplied none. Read ctx.Deadline(); a package can only shorten a budget, never extend one.

open as a page

Which request-scoped data may travel in a context.Context across teams, and how do you hold that line?

level: principalimportance: nice to knowfreq 30%

basics

~20 s

Admit only data that is about the request, immutable, and optional for whoever reads it — trace and request ids. Anything a function cannot run without belongs in its signature. Enforce it with one package owning the key types and accessors.

open as a page