Why is storing a context.Context in a struct considered an anti-pattern? Are there exceptions?
Question 271MediumGo 1.22 to 1.25
The context package docs say: "Do not store Contexts inside a struct type; instead, pass a Context explicitly to each function that needs it." The reasons:
- Lifetime mismatch: a context belongs to one operation, while a struct usually lives across many. A
Clientholding the context of the request that created it will fail every later call once that first request finishes. - Hidden control flow: callers cannot give different deadlines to different method calls.
- Unclear API: a signature like
func (c *Client) Get(url string)hides the fact that the call can block or be canceled.
// Anti-pattern
type Worker struct {
ctx context.Context
db *sql.DB
}
func (w *Worker) Process(id int) error { /* uses w.ctx */ return nil }
// Idiomatic
type Worker struct{ db *sql.DB }
func (w *Worker) Process(ctx context.Context, id int) error { /* ... */ return nil }
Accepted exceptions:
- Structs that are a single operation, such as
http.Request(its context field is set throughWithContext) or an iterator/stream object used only inside one call. - A long-running component that owns its own lifecycle, which keeps a root context and a cancel function for
Close()/Stop(). - Backward compatibility, when a method signature cannot change.
Convention: ctx is the first parameter and is named ctx. To add context support without breaking an API, add a second method (Query alongside QueryContext).
More on Context
- Q269How is Value lookup implemented, and what are its performance implications?
- Q270What belongs in context values and what doesn't?
- Q272context.Background() vs context.TODO(): when do you use each, and why not pass nil?
- Q273What is context.AfterFunc and what are the semantics of its stop function?
- Q274How do you make a blocking call that doesn't accept a context (e.g. net.Conn.Read) cancelable?
- Q275What does context.WithoutCancel do and when would you use it?