Context-Driven Cancellation
`context.Context` is Go's standard mechanism for cancelling long-running operations: a deadline, a parent-child cancellation tree, and a request-scoped value bag. This snippet shows `context.WithTimeout` to bound a function's runtime, the `select { case <-ctx.Done(): ... }` pattern in workers, and how to attach a request-id value. Pass `ctx` as the first argument to every function that does I/O or has a chance of blocking.
158 views
3
package main
import (
"context"
"fmt"
"time"
)
func slowFetch(ctx context.Context) (string, error) {
select {
case <-time.After(200 * time.Millisecond):
return "data", nil
case <-ctx.Done():
return "", ctx.Err()
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
defer cancel()
val, err := slowFetch(ctx)
if err != nil {
fmt.Println("err:", err) // context deadline exceeded
return
}
fmt.Println("got:", val)
}context.WithTimeout(parent, d) returns a derived context that auto-cancels after d, plus a cancel function you should defer even if the timeout fires first (it releases internal resources). The callee blocks in a select waiting for either work to complete or ctx.Done() to close, returning ctx.Err() (context.DeadlineExceeded or context.Canceled) on timeout. This pattern is what every Go HTTP client, database driver, and gRPC stub already implements internally; threading the context through your own functions lets callers tighten or extend the deadline without changing your code.
package main
import (
"context"
"fmt"
"time"
)
func worker(ctx context.Context, jobs <-chan int) {
for {
select {
case <-ctx.Done():
fmt.Println("worker exiting:", ctx.Err())
return
case j, ok := <-jobs:
if !ok {
return
}
fmt.Println("processed", j)
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
jobs := make(chan int, 5)
go worker(ctx, jobs)
for i := 1; i <= 3; i++ {
jobs <- i
}
time.Sleep(20 * time.Millisecond)
cancel() // tells the worker to exit
time.Sleep(20 * time.Millisecond)
}Long-running worker goroutines should always check ctx.Done() so callers can shut them down without leaking. context.WithCancel(parent) gives you a context plus a cancel() function you call when shutdown is desired; closing the context's done channel signals every selecting goroutine. Pair this with the worker-pool pattern: pass the same context into every worker, and a single cancel() call drains all of them. Always return promptly from ctx.Done(); if you need to flush state, do so before returning, but do not start new long operations after cancellation.
package main
import (
"context"
"fmt"
)
type ctxKey string
const requestIDKey ctxKey = "request-id"
func handle(ctx context.Context) {
if id, ok := ctx.Value(requestIDKey).(string); ok {
fmt.Println("handling request", id)
} else {
fmt.Println("no request id")
}
}
func main() {
ctx := context.WithValue(context.Background(), requestIDKey, "req-42")
handle(ctx)
}context.WithValue(parent, key, value) attaches a key/value pair to the context, propagated to every derived context. Use this for request-scoped metadata that genuinely should travel with the request (request id, correlation id, trace span); do NOT use it for optional function arguments. Always declare a private key type (type ctxKey string) so external packages cannot accidentally collide with your key. The lookup form is ctx.Value(key).(string) with a comma-ok type assertion since the underlying value is interface{}. Keep the value tree shallow: every layer of WithValue allocates a new wrapper and slows lookups.
