Right, so you’ve heard the rule: “Don’t store context.Context in a struct.” You’ve probably nodded along, but let’s be honest, you’re also thinking, “But… why? It seems so convenient.” I get it. It feels like the perfect place to stash that cancellation signal so all your methods can use it. It’s a trap. Let’s break down exactly why this is the software equivalent of storing nitroglycerin in a shoebox—it might be fine until it isn’t, and when it goes wrong, it’s spectacular.

The cardinal rule is this: a context.Context is meant to flow through your call stack, request-scoped like a river, not be stored in a field, stagnant like a pond. It’s about the propagation of cancellation and deadlines, not about long-term storage.

The One (and only) Valid Exception

Before we dive into the horrors, let’s acknowledge the one scenario where this is sometimes acceptable. If your struct is itself a direct translation of a request-scoped operation and its lifetime is shorter than or equal to the request itself, it’s a gray area. Think of a object created to handle a single API call, used, and then discarded. Even then, you must be hyper-vigilant.

// This is... maybe okay? But still smelly.
type RequestProcessor struct {
    ctx context.Context
    // ... other request-scoped fields
}

func NewRequestProcessor(ctx context.Context) *RequestProcessor {
    return &RequestProcessor{ctx: ctx}
}

func (r *RequestProcessor) Process() error {
    result, err := someDatabaseCall(r.ctx) // Using the stored context
    if err != nil {
        return err
    }
    // ... process result
    return nil
}
// This struct is created and dies within the lifespan of a single request.

The moment this struct gets cached, reused, or lives beyond the initial request, this pattern explodes. Which brings us to the main event…

The Primary Reason: Context Lifetimes

The most common and devastating mistake is storing a context from one request and then using it on another. The context from Request A is a snapshot of a moment in time. When Request A finishes, its context is cancelled. If you somehow reuse that same struct for Request B, you’re handing it a pre-cancelled context. Your database calls, HTTP requests, and any other blocking operations will instantly fail. Good luck debugging that.

// A horrific example that will cause random, baffling failures
type BadDatabaseClient struct {
    ctx context.Context
}

func (c *BadDatabaseClient) Query(query string) (*Result, error) {
    // This uses whatever ancient, cancelled context was stored during creation.
    return databaseQuery(c.ctx, query)
}

// Meanwhile, in your server code...
client := &BadDatabaseClient{ctx: ctx} // Created for request #1
go func() {
    time.Sleep(5 * time.Second)
    // Now we're in request #2, but using the context from the long-dead request #1
    result, err := client.Query("SELECT * FROM users") // Instant cancellation!
    if err != nil {
        log.Fatalf("Why does this always fail on Tuesdays?! %v", err) // 🤦
    }
}()

The error messages will point everywhere except the actual problem: a context cancelled five seconds ago. You’ll tear your hair out.

It Defeats The Whole Purpose

The entire point of context is to be able to compose cancellation signals from different parts of your call graph. If you hard-code a context at the top level into a struct, you lose the ability to adjust the context for a specific call.

What if you want a longer timeout for one particular method call on that struct? Tough luck. You’re stuck with the original. The correct pattern is to pass the context as the first argument to every method that needs it. This is explicit, clear, and composable.

// The CORRECT way. This is what we want to see.
type GoodDatabaseClient struct {
    // ... connection info, stats, whatever
}

// Notice the ctx parameter! This is the golden rule.
func (c *GoodDatabaseClient) Query(ctx context.Context, query string) (*Result, error) {
    // Now we can use the fresh, current context for *this specific call*.
    return databaseQuery(ctx, query)
}

Now, the caller is in control. They can derive a new context with a different deadline just for this query, and your GoodDatabaseClient works perfectly in every scenario.

Storing Derivatives: An Even Sneakier Trap

“Fine,” you say, “I won’t store the original context. I’ll store a derivative context.WithCancel in my struct so I can cancel it later.” This is a more sophisticated mistake, but a mistake nonetheless.

You’re now mixing concerns. The struct is now responsible for the lifecycle of a context, which is a separate, tricky responsibility. You must ensure you call the cancel() function, or you’ll cause memory leaks. This adds significant cognitive load and complexity to what was probably a simple data-holding struct. The need to do this is often a design smell, suggesting your cancellation logic is in the wrong place. The cancellation signal should almost always be managed higher up in the call stack, not buried inside a helper struct.

So, repeat after me: First parameter, every time. Make it muscle memory. Your future self, staring at a monitor at 2 a.m. trying to figure out why everything is mysteriously failing, will thank you.