Go Context
Go Context
context.Context is the standard way to carry deadlines, cancellations, and request-scoped values across API boundaries and goroutines. Every production Go service uses it. Pass ctx as the first parameter of every function that does I/O.
The Four Constructors
ctx := context.Background() // root — use at main, top of request handler
ctx := context.TODO() // placeholder — "I'll add context later"
ctx, cancel := context.WithCancel(parent) // manual cancel
ctx, cancel := context.WithTimeout(parent, 5*time.Second) // cancel after duration
ctx, cancel := context.WithDeadline(parent, time.Now().Add(5*time.Second)) // cancel at time
ctx := context.WithValue(parent, key, value) // attach request-scoped data
Always call cancel() for WithCancel/WithTimeout/WithDeadline — prevents goroutine leak.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel() // always defer immediately after creation
Propagating Context
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := fetchUser(ctx, 42)
}
func fetchUser(ctx context.Context, id int) (User, error) {
// pass ctx down to every I/O call
row, err := db.QueryContext(ctx, "SELECT * FROM users WHERE id = ?", id)
if err != nil {
return User{}, err
}
// ...
}
Rule: ctx is always the first parameter, never stored in a struct.
Checking Cancellation
func worker(ctx context.Context, jobs <-chan Job) {
for {
select {
case <-ctx.Done():
// context cancelled or timed out
fmt.Println("worker stopping:", ctx.Err()) // context.Canceled or context.DeadlineExceeded
return
case job, ok := <-jobs:
if !ok {
return
}
process(job)
}
}
}
ctx.Err() returns:
context.Canceled— parent calledcancel()context.DeadlineExceeded— timeout/deadline passednil— still active
Context in HTTP Servers
http.Request already carries a context — use r.Context().
func handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // already has request deadline if server has WriteTimeout
// add a tighter internal timeout
ctx, cancel := context.WithTimeout(ctx, 3*time.Second)
defer cancel()
user, err := db.QueryContext(ctx, "SELECT ...")
if err != nil {
if ctx.Err() != nil {
http.Error(w, "request timeout", http.StatusGatewayTimeout)
return
}
http.Error(w, "db error", http.StatusInternalServerError)
return
}
}
Context Values — WithValue
Use sparingly. Only for request-scoped data that doesn't fit function parameters: request ID, auth user, trace ID.
// use unexported key type to avoid collision
type ctxKey string
const requestIDKey ctxKey = "requestID"
// middleware attaches value
func withRequestID(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
id := uuid.New().String()
ctx := context.WithValue(r.Context(), requestIDKey, id)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
// downstream extracts value
func getRequestID(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey).(string)
return id
}
Don'ts:
- Don't use
stringas key type — collisions across packages - Don't pass optional function params via context
- Don't store mutable values — context values are read-only
Context with goroutines
func main() {
ctx, cancel := context.WithCancel(context.Background())
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
worker(ctx, id)
}(i)
}
time.Sleep(3 * time.Second)
cancel() // signal all workers to stop
wg.Wait()
}
Stdlib Functions That Accept Context
// database/sql
db.QueryContext(ctx, query, args...)
db.ExecContext(ctx, query, args...)
// net/http client
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
client.Do(req)
// net package
dialer.DialContext(ctx, "tcp", addr)
Common Mistakes
| Mistake | Fix |
|---|---|
context.WithTimeout without defer cancel() | Always defer cancel() immediately |
| Storing context in a struct field | Pass as function parameter instead |
Using string as context key | Use unexported custom type |
Ignoring ctx.Err() after error | Check if timeout/cancel caused the error |
context.Background() deep inside call stack | Thread ctx from the top — never create Background mid-call |
Interview Talking Points
- "Context solves the goroutine leak problem — without cancellation, a goroutine doing a DB query has no way to know the client already left."
- "context.WithTimeout wraps WithDeadline. Both create a child context that cancels at a point in time. The cancel() func is for early cancel before the deadline."
- "WithValue is for cross-cutting concerns (trace ID, auth user) — not for business logic params. If it's business logic, make it a real function parameter."
Related
- [[Go/Channels]] — ctx.Done() is a receive-only channel
- [[Go/HTTP Server]] — context in request handlers
- [[Go/HTTP Clients]] — NewRequestWithContext
- [[synthesis/Concurrency Deep Dive]] — concurrency patterns