Background Context Instead Of Context Parameter
ID |
go.context_params_reused_instead_creating |
Severity |
high |
Remediation Complexity |
trivial |
Remediation Risk |
low |
Remediation Effort |
low |
Resource |
Efficiency |
Language |
Go |
Tags |
best-practice, efficiency |
Description
Reports context.Background() called inside a function that already receives a context.Context
parameter, where the parameter should have been propagated.
Only context.Background() is reported. context.TODO() is left to go.context_todo, which
already flags every occurrence of it, so the two rules never report the same line. A context
parameter that cannot be propagated — unnamed, or the blank identifier _ context.Context — does
not trigger the rule.
func main, func init and test entry points take no context.Context parameter, so they cannot
match. Neither can a goroutine or callback body, because the enclosing function is resolved to the
innermost one and a function literal declares its own parameter list: a closure meant to outlive
the request is therefore never reported.
Rationale
The parameter is the caller’s context. It carries three things: the deadline for the operation in flight, the cancellation signal for it, and the request-scoped values — trace id, tenant, authenticated principal — that every downstream hop expects to find.
context.Background() is a fresh root with none of them. Work started from it ignores the caller’s
deadline, keeps running after the client has disconnected and the caller’s context has been
cancelled, and drops the values that make a request traceable. What was meant to be a bounded piece
of work becomes an unbounded one, holding a connection, a lock or a goroutine that nothing will
ever cancel.
func fetchProfile(ctx context.Context, id string) (*Profile, error) {
root := context.Background() // FLAW — ctx is already in scope; propagate it
timeout, cancel := context.WithTimeout(root, 2*time.Second)
defer cancel()
return query(timeout, id)
}
Remediation
Derive from the parameter instead of from a new root. The child then inherits the caller’s deadline and cancellation, and tightens them if it needs to.
func fetchProfile(ctx context.Context, id string) (*Profile, error) {
timeout, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
return query(timeout, id)
}
context.Background() belongs at a genuine root — main, a server’s start-up path, a background
worker’s own loop — where there is no caller and therefore no context to inherit.
When work is deliberately meant to outlive the request, say so rather than starting from a new
root: context.WithoutCancel(ctx) detaches the cancellation while keeping the request’s values.