Context
The context package provides a standard way to carry deadlines, cancellation signals, and request-scoped values across goroutines and API boundaries. Pass a context as the first argument to every function that starts a goroutine or makes a blocking call.
The context.Context Interface
type Context interface {
Deadline() (deadline time.Time, ok bool)
Done() <-chan struct{}
Err() error
Value(key any) any
}Done()— returns a channel that is closed when the context is cancelled or times out.Err()— returnscontext.Canceledorcontext.DeadlineExceededafterDoneis closed.Value(key)— retrieves a request-scoped value stored withWithValue.
context.Background and context.TODO
Use context.Background() as the root context in main, tests, and top-level initialisers. Use context.TODO() as a placeholder when you are not yet sure which context to use:
package main
import (
"context"
"fmt"
)
func main() {
ctx := context.Background()
fmt.Println(ctx) // context.Background
}WithCancel
WithCancel creates a derived context with a cancel function. Calling cancel() closes ctx.Done():
package main
import (
"context"
"fmt"
"sync"
)
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
for {
select {
case <-ctx.Done():
fmt.Printf("worker %d cancelled: %v\n", id, ctx.Err())
return
default:
// do work
fmt.Printf("worker %d working\n", id)
return // simulate a single unit of work
}
}
}
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // always cancel to release resources
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
cancel() // cancel all workers
wg.Wait()
}Idiomatic Go: Always call the cancel function returned by
WithCancel,WithTimeout, orWithDeadline. Usedefer cancel()immediately after the call.
WithTimeout and WithDeadline
WithTimeout(ctx, duration) cancels the context after the given duration. WithDeadline(ctx, time) cancels it at a specific time:
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(), 100*time.Millisecond)
defer cancel()
result, err := slowFetch(ctx)
if err != nil {
fmt.Println("error:", err) // error: context deadline exceeded
return
}
fmt.Println(result)
}WithValue
Store request-scoped values (e.g., trace IDs, user info) in a context. Always use a private, unexported key type to avoid collisions:
package main
import (
"context"
"fmt"
)
type contextKey string
const requestIDKey contextKey = "requestID"
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func RequestID(ctx context.Context) (string, bool) {
id, ok := ctx.Value(requestIDKey).(string)
return id, ok
}
func handle(ctx context.Context) {
if id, ok := RequestID(ctx); ok {
fmt.Println("handling request:", id)
}
}
func main() {
ctx := WithRequestID(context.Background(), "req-abc-123")
handle(ctx)
}Do not store optional parameters or configuration in context — only request-scoped data that crosses package boundaries.
Propagating Context to HTTP Requests
net/http integrates with context: every http.Request carries a context accessible via r.Context():
package main
import (
"context"
"fmt"
"net/http"
"time"
)
func fetchData(ctx context.Context, url string) error {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
fmt.Println("status:", resp.Status)
return nil
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := fetchData(ctx, "https://go.dev"); err != nil {
fmt.Println("error:", err)
}
}Key Takeaways
- Pass
context.Contextas the first argument to every function that starts a goroutine or makes a blocking call. - Always call the cancel function; use
defer cancel()immediately afterWithCancel/WithTimeout/WithDeadline. ctx.Done()is closed when the context is cancelled; check it inselectstatements.- Use an unexported key type with
WithValueto avoid key collisions. - Do not store function parameters or configuration in context — use it for request-scoped cross-cutting concerns only.