What context.Context Is in Go
What the context package is for, the four methods of context.Context, where Background and TODO fit, and why ctx is always the first parameter.
Imagine a web request that starts three database queries and an API call. The user closes the browser tab after one second. Without some way to tell that work to stop, your server keeps running all four operations for nobody. context.Context is Go's standard way to carry a stop signal and a deadline through every function involved in one piece of work.
The Interface
context.Context is an interface with four methods:
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}
Done()returns a channel that is closed when the work should stop. This is the part you use most.Err()says why it stopped:context.Canceledorcontext.DeadlineExceeded. It returnsnilwhile the context is still active.Deadline()reports the time limit, if there is one.Value(key)looks up request-scoped data such as a request ID.
You almost never implement this interface yourself. You create contexts with functions from the context package and pass them along.
Background: the Root Context
Every context tree starts from a root. context.Background() returns an empty context that is never cancelled and has no deadline:
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
fmt.Println("err:", ctx.Err())
_, hasDeadline := ctx.Deadline()
fmt.Println("has deadline:", hasDeadline)
fmt.Println("done channel is nil:", ctx.Done() == nil)
}
Output:
err: <nil>
has deadline: false
done channel is nil: true
Use context.Background() in main, in tests, and at the top of background jobs. context.TODO() behaves the same way but signals "I have not decided which context belongs here yet". It is a marker for unfinished code.
Deriving Child Contexts
You add behavior by wrapping a parent context. Each wrapper returns a new child context:
context.Background()
-> WithTimeout(5s) child: also stops after 5 seconds
-> WithValue(reqID) grandchild: also carries a request ID
-> WithCancel grandchild: can also be stopped by hand
When a parent is cancelled, every child under it is cancelled too. A child can never outlive its parent. This is what lets one cancelled request stop all the work it started.
package main
import (
"context"
"fmt"
)
func main() {
parent, cancelParent := context.WithCancel(context.Background())
child, cancelChild := context.WithCancel(parent)
defer cancelChild()
cancelParent()
<-child.Done()
fmt.Println("child stopped because:", child.Err())
}
This prints child stopped because: context canceled. Cancelling the parent closed the child's Done channel too.
ctx Is the First Parameter
By convention, a function that takes a context receives it as its first parameter, named ctx:
func FetchUser(ctx context.Context, id int) (*User, error)
func (s *Store) Save(ctx context.Context, order Order) error
The standard library follows this: http.NewRequestWithContext(ctx, ...), db.QueryContext(ctx, ...), exec.CommandContext(ctx, ...).
Do not store a context in a struct:
Pass the context into each call instead of saving it in a struct field. A context belongs to one request or operation, while a struct usually lives much longer. A stored context ends up cancelling unrelated later work, or never cancelling anything. The Go documentation recommends passing it explicitly.
Never pass a nil context:
Passing nil as a context makes functions that call ctx.Done() or ctx.Err() panic with invalid memory address or nil pointer dereference. If you have nothing better, pass context.Background() or context.TODO().
The mental model:
A context is a remote control for one piece of work: it can say "stop", say "you have until this time", and carry a few labels. Every function doing part of that work holds the same remote.
Summary
context.Context carries cancellation, deadlines and request-scoped values through the functions that do one piece of work. Start trees with context.Background(), derive children with the With... functions, and know that cancelling a parent cancels every child. Pass the context as the first parameter named ctx, never store it in a struct, and never pass nil.