← Files GophersARCHIVED FILE

skills/go-context/references/http-and-db.md

3.22 KB · Oct 5, 2026 · 18:31 UTC

↓ Download file

# Context in HTTP Handlers, Clients, and Databases

The Go standard library is consistent: any I/O that can block has a `*Context` variant. Always use those — never the legacy non-context API in code that runs inside a request.

## HTTP Handlers

`*http.Request` carries the request's context. It is cancelled when the client disconnects or the server timeout fires.

```go
func handleOrder(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    order, err := svc.Get(ctx, mux.Vars(r)["id"])
    if err != nil {
        // ctx may be cancelled; reply only if not done
        http.Error(w, err.Error(), 500)
        return
    }
    _ = json.NewEncoder(w).Encode(order)
}
```

Middleware can attach values (request ID, principal) by replacing the request:

```go
func WithRequestID(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        ctx := WithReqID(r.Context(), uuid.NewString())
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}
```

## HTTP Clients

Always use `http.NewRequestWithContext`. The legacy `http.NewRequest` produces a request with no context, and the resulting call cannot be cancelled cleanly.

```go
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()

req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
    return nil, err
}
resp, err := http.DefaultClient.Do(req)
```

On cancellation, the HTTP transport closes the connection and `Do` returns an error that wraps `context.Canceled` or `context.DeadlineExceeded`.

### Retries

Retries should observe `ctx` between attempts:

```go
for attempt := 0; attempt < maxAttempts; attempt++ {
    if err := ctx.Err(); err != nil {
        return err // parent gave up, do not retry
    }
    if resp, err := doOnce(ctx); err == nil {
        return resp
    }
    select {
    case <-ctx.Done():
        return ctx.Err()
    case <-time.After(backoff(attempt)):
    }
}
```

## Database Operations

`database/sql` exposes `QueryContext`, `ExecContext`, `BeginTx`, `PingContext`. Use them everywhere. The driver will cancel the in-flight query when `ctx` is done.

```go
rows, err := db.QueryContext(ctx, "SELECT id, name FROM users WHERE tenant = $1", t)
if err != nil {
    return nil, fmt.Errorf("loading users: %w", err)
}
defer rows.Close()
```

For transactions:

```go
tx, err := db.BeginTx(ctx, nil)
if err != nil {
    return err
}
defer tx.Rollback() // safe even after Commit

if _, err := tx.ExecContext(ctx, insertSQL, ...); err != nil {
    return err
}
return tx.Commit()
```

A cancelled `ctx` rolls back the transaction at the driver level.

## gRPC

gRPC client and server stubs already take `ctx` as the first argument. The metadata package extracts values for tracing and auth:

```go
md, _ := metadata.FromIncomingContext(ctx)
authz := md.Get("authorization")
```

Outbound calls inherit the inbound `ctx` automatically.

## Anti-Patterns

- `http.Get(url)` inside a handler — no context, cannot be cancelled.
- `db.Query` instead of `db.QueryContext` — same.
- `r.Context()` ignored, building a new `context.Background()` for downstream calls — defeats request cancellation.
- Caching `r.Context()` past the request lifetime — it is dead the moment the handler returns.

SHA-256: 275bab864efc24183ef7486ccc5f7cbda253ebc4557637d0bd735eeb0f7abb90