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

context.context, and learning to thread it through

Why context.Context belongs as the first argument everywhere in Go, and the bug I caused by not threading it through.

A Go source file showing a context parameter

I used to treat context.Context as a thing you added later, once you needed cancellation. That was the wrong model, and it cost me an afternoon. The lesson: thread it through from the start, as the first argument of anything that does I/O, and stop pretending you can bolt it on at the end.

The bug was a request handler that fired off three downstream calls. The client gave up after a couple of seconds, but my handler didn't know that, because I'd passed context.Background() into the database call rather than the request's own context. So the user had long since walked away, and my code was still dutifully waiting on a slow query, holding a connection it had no reason to hold. Multiply that by a busy afternoon and you've got a pool exhausted by work nobody is waiting for.

The fix was almost embarrassingly small. Take the ctx that http.Request already carries, and pass that down, all the way to the query:

func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    row := h.db.QueryRowContext(ctx, query, id)
    // ...
}

When the client disconnects, the context cancels, the query gets cancelled with it, and the connection comes back. The whole chain learns about the abandonment because the cancellation rode along the same path the request did.

So now the rule, for me, is simple and slightly dogmatic: ctx context.Context is the first parameter of any function that talks to the network, a database, or a disk, and you pass the real one down rather than inventing a fresh Background() halfway through. It looks like ceremony when you write it. It stops being ceremony the first time a timeout actually propagates the way it should.