What is context.Context in Go, and why is it passed as a function's first parameter?
answer
- a caller that gave up must say so
- Go has no goroutine-local storage
- one value threaded through every call
- cancellation, a deadline, request-scoped values
- conventionally first, conventionally named ctx
basics
~20 scontext.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 linesfunc (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
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.
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.
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.
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