The root of every context tree
You are writing a background worker that polls a message queue every second. The worker needs a context.Context so you can shut it down gracefully when the server stops. You look at your function signature. It requires a context. You look at your main function. There is no context there. You need to create one.
You call context.Background(). That gives you the root.
Now you are writing a helper function. It needs a context, but you haven't wired up the caller yet. You don't want to break the build. You pass context.TODO(). The code compiles. You have a flag for yourself to fix the wiring later.
context.Background() and context.TODO() are the entry points to Go's context system. They create the empty context that sits at the top of the tree. Everything else derives from them.
What the empty context actually is
A context in Go is a carrier. It carries deadlines, cancellation signals, and key-value pairs down the call stack. The context.Context interface defines four methods: Deadline, Done, Err, and Value.
The empty context returned by Background and TODO implements this interface with no behavior. It has no deadline. It has no values. It never cancels. The Done channel is always nil. The Err method always returns nil.
Think of the empty context like the mains power outlet in a wall. It is the source. It is always available. It doesn't have a timer attached to it. You can plug a device into it, or you can plug a power strip into it and add timers and switches downstream. The outlet itself just provides the connection point.
context.Background() and context.TODO() return the exact same object. They are identical under the hood. The implementation is a private singleton struct inside the context package. Calling context.Background() == context.TODO() evaluates to true.
The difference is purely semantic. Background means "this is the root of a context tree." TODO means "I know a context should come from somewhere else, but I haven't implemented that yet." TODO is a placeholder that tells the next developer to look at this line and wire up the real context.
Minimal example
Here is the simplest way to create a root context and pass it to a function.
package main
import (
"context"
"fmt"
"time"
)
// doWork simulates a task that respects cancellation.
// The context allows the caller to signal "stop now".
func doWork(ctx context.Context) {
select {
case <-ctx.Done():
// Exit immediately if the context is cancelled.
fmt.Println("work stopped")
return
case <-time.After(500 * time.Millisecond):
fmt.Println("work finished")
}
}
func main() {
// Background provides the empty root context.
// It has no deadline, no values, and never cancels.
rootCtx := context.Background()
// Pass the root context to the worker.
// Since rootCtx never cancels, doWork will run until the timer fires.
doWork(rootCtx)
}
The code creates a root context and passes it down. The worker checks ctx.Done(). Because rootCtx is the empty context, ctx.Done() is nil. Reading from a nil channel blocks forever. The select statement waits on the timer. The timer fires after 500 milliseconds. The worker prints "work finished" and returns.
If you derive a new context with context.WithCancel and call the cancel function, ctx.Done() closes. The select unblocks on the first case. The worker stops early.
Walkthrough: compile time and runtime
At compile time, the compiler sees context.Background() and knows it returns a value of type context.Context. The Context type is an interface. The compiler checks that the return value satisfies the interface. It does. The compiler generates a call to the internal function that returns the singleton.
At runtime, context.Background() returns a pointer to a private struct. The struct implements the four methods. Deadline returns time.Time{} and false. Done returns nil. Err returns nil. Value returns nil.
Calling Background is extremely cheap. It returns a cached pointer. There is no allocation. There is no lock. You can call it millions of times per second with zero overhead.
context.TODO() returns the same pointer. The runtime cannot tell the difference. The distinction exists only in the source code.
Convention dictates that context is always the first parameter to a function, and it is conventionally named ctx. Functions that accept a context should respect cancellation. If a function takes a context, it should check ctx.Done() or pass the context to downstream calls that do.
Realistic example: HTTP handler and background worker
In a real application, contexts flow from the entry point down to the database. HTTP handlers get a context from the request. Background workers get a context from the server lifecycle.
Here is a server that handles requests and runs a background polling goroutine.
package main
import (
"context"
"fmt"
"net/http"
"time"
)
// handleRequest processes an incoming HTTP request.
// The request provides a context that cancels when the client disconnects.
func handleRequest(w http.ResponseWriter, r *http.Request) {
// req.Context() extracts the context from the request.
// This context carries the client's deadline and cancellation signal.
ctx := r.Context()
// Pass the context to downstream calls.
// If the client disconnects, the context cancels and the query stops.
if err := doDatabaseWork(ctx); err != nil {
http.Error(w, "internal error", http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
fmt.Fprintln(w, "success")
}
// doDatabaseWork simulates a query that respects the context.
func doDatabaseWork(ctx context.Context) error {
select {
case <-ctx.Done():
// Return the context error to propagate cancellation.
return ctx.Err()
case <-time.After(200 * time.Millisecond):
return nil
}
}
// pollQueue runs indefinitely until the context is cancelled.
func pollQueue(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("poller stopping")
return
case <-time.After(1 * time.Second):
fmt.Println("polling...")
}
}
}
func main() {
// Background creates the root context for the server lifecycle.
// The server itself is not tied to a single request.
serverCtx := context.Background()
// Derive a cancellable context for the background worker.
// This allows main to stop the worker when the server shuts down.
ctx, cancel := context.WithCancel(serverCtx)
defer cancel()
// Start the background worker in a goroutine.
go pollQueue(ctx)
// Set up the HTTP handler.
http.HandleFunc("/", handleRequest)
fmt.Println("server starting on :8080")
// In a real app, you would handle signals and call cancel() here.
http.ListenAndServe(":8080", nil)
}
The HTTP handler uses r.Context(). This context is created by the http.Server for each request. It cancels when the client closes the connection or when the server times out the request. Passing this context to doDatabaseWork ensures that database queries don't run forever if the client goes away.
The background worker uses context.Background() as the root, then derives a cancellable context with context.WithCancel. The cancel function is deferred. When main returns, the cancel function runs. The context's Done channel closes. The pollQueue goroutine sees the cancellation and exits. This prevents a goroutine leak.
Context flows down. Cancellation flows up. The root context starts the tree. Derived contexts add behavior.
Pitfalls and compiler errors
Passing nil as a context is a common mistake. The context.Context interface is satisfied by nil. The compiler allows nil to be passed where a context is expected. Many functions in the standard library panic if you pass nil. You get a runtime panic with runtime error: invalid memory address or nil pointer dereference. Always pass a valid context. Use context.Background() if you have no other option.
Storing values in the root context is impossible. The empty context has no values. You must derive a new context with context.WithValue to add data. Deriving creates a new node in the tree. The new context wraps the parent. Values are looked up by walking up the tree.
Goroutine leaks happen when a goroutine waits on a channel that never closes. If you spawn a goroutine with context.Background() and the goroutine blocks on a channel, and you never close that channel, the goroutine leaks. Background never cancels. You must use context.WithCancel or context.WithTimeout to create a cancellation path. The worst goroutine bug is the one that never logs. Always ensure background goroutines have a way to exit.
The compiler rejects code that passes the wrong type to a context parameter. If you pass a string where a context is expected, you get cannot use "value" (untyped string constant) as context.Context value in argument. If you forget to import the package, you get undefined: context. If you import the package and don't use it, you get imported and not used. Go's compiler is strict about unused imports.
Convention aside: gofmt is mandatory. Don't argue about indentation or brace placement. Let the tool decide. Most editors run gofmt on save. The community expects formatted code. Focus your energy on logic, not formatting.
Another convention: public names start with a capital letter. Private names start with a lowercase letter. There are no keywords like public or private. The capitalization controls visibility. context.Background is public because it starts with a capital letter. The internal struct is private because it starts with a lowercase letter. This prevents users from creating their own context implementations that bypass the interface contract.
When to use Background, TODO, or something else
Use context.Background() when you are at the entry point of your program and need to create the root of a context tree. Use context.TODO() when a function requires a context but you haven't wired up the correct source yet and need a placeholder to keep the code compiling. Use req.Context() when you are inside an HTTP handler and need the context associated with the current request. Use context.WithCancel() when you need to derive a new context that can be cancelled independently of its parent. Use context.WithTimeout() when a task must complete within a specific duration or be cancelled. Use context.WithValue() when you need to pass request-scoped data down the call stack, and only when no other API exists to carry that data.
Background is the root. TODO is a promise to fix it later. Context is plumbing. Run it through every long-lived call site.