Ramblings of an aging IT geek
← Ramblings of an aging IT geek
golang

passing context.Context until it finally clicked

Why context.Context belongs as the first argument everywhere, and how cancellation propagation stopped a goroutine leak in a Go service.

A terminal with Go source on screen

For a long time context.Context felt like ceremony. Every function grew a ctx context.Context first argument, I dutifully passed it down, and I could not have told you what it bought me. So I did the lazy thing in a few hot paths and reached for context.Background() whenever threading the real one felt tedious.

Then a handler started leaking goroutines. A request would come in, fan out to three downstream calls, and the client would hang up after a second. The downstream calls had no idea anyone had left. They sat there for their full thirty-second timeout, holding connections, while new requests piled in behind them. The fix was the thing I had been skipping: pass the request's context down so cancellation actually propagates.

func fetch(ctx context.Context, id string) (*Item, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", url(id), nil)
    return do(req)
}

When the client disconnects, the server cancels the request context, the cancellation walks down the tree, and every in-flight call gives up at once. The goroutines drain instead of accumulating. That is the whole point, and I had been opting out of it one Background() at a time.

The rule I follow now is boring and I no longer fight it: ctx is the first parameter, you never store it in a struct, and you only mint a fresh one at a genuine boundary like a background job. It is not ceremony. It is the wiring that lets a cancelled request actually stop doing work.