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

passing context.Context everywhere, and why it's worth it

Threading context.Context through a Go service as the first parameter of every call, what it bought in cancellation and deadlines, and why the ceremony stops feeling like ceremony.

A code editor showing a Go function signature taking a context

For ages I treated context.Context as a thing you bolt onto HTTP handlers because the framework hands you one. It sat at the edge of my services and went no further. Then a slow downstream call held a request open long after the client had given up, and I realised the whole point is to thread it all the way down, not park it at the door.

The rule is unglamorous and you've read it a hundred times: ctx context.Context is the first parameter of any function that does I/O or might block, and you pass it down, never store it in a struct. The first time you do this to an existing codebase it feels like vandalism. Every signature grows a parameter, every call site gains a ctx, and the diff is enormous and boring. I kept asking whether it was worth the churn.

It was, the first time a deadline actually fired. A client disconnected, the context cancelled, and the database query down at the bottom of the stack returned context.Canceled instead of grinding on against a connection nobody was waiting for. I didn't write any cancellation logic. The query just respected the context it was handed, because I'd finally bothered to hand it one.

The part that took longest to internalise is that context is a chain, not a value. Each layer can wrap it with its own deadline, and the tightest one wins. Once that clicked, the ceremony stopped reading as ceremony. The ctx in every signature isn't boilerplate, it's the cancellation and the deadline travelling alongside the work, which is exactly where they need to be.