About
This skill provides guidance on proper context.Context usage in Go, covering propagation, cancellation, timeouts, deadlines, and values while highlighting common anti-patterns. Use it for questions about context implementation but not for broader concurrency patterns, HTTP middleware, or error handling scenarios.
Quick Install
Claude Code
Recommendednpx skills add eduardo-sl/go-agent-skills -a claude-code/plugin add https://github.com/eduardo-sl/go-agent-skillsgit clone https://github.com/eduardo-sl/go-agent-skills.git ~/.claude/skills/go-contextCopy and paste this command in Claude Code to install this skill
Documentation
Go Context
context.Context controls cancellation, deadlines, and request-scoped values
across API boundaries. Misusing it causes goroutine leaks, orphaned work,
and subtle production bugs.
1. Core Rules
Context is always the first parameter:
// ✅ Good — context is first
func GetUser(ctx context.Context, id string) (*User, error)
func (s *Service) Process(ctx context.Context, req Request) error
// ❌ Bad — context buried in the middle or end
func GetUser(id string, ctx context.Context) (*User, error)
func Process(req Request, ctx context.Context) error
NEVER store context in a struct:
// ❌ Bad — context stored in struct
type Server struct {
ctx context.Context // NEVER do this
cancel context.CancelFunc
}
// ✅ Good — pass context through method parameters
func (s *Server) Shutdown(ctx context.Context) error {
return s.httpServer.Shutdown(ctx)
}
Context represents the lifetime of a single operation, not the lifetime of an object.
NEVER pass nil context:
// ❌ Bad
doSomething(nil, data)
// ✅ Good — use context.TODO() if unsure which context to use
doSomething(context.TODO(), data)
// ✅ Good — use context.Background() for top-level/main
doSomething(context.Background(), data)
2. Cancellation
Always defer cancel:
// ✅ Good — cancel called even if operation succeeds
ctx, cancel := context.WithCancel(parentCtx)
defer cancel()
result, err := longOperation(ctx)
Failing to call cancel leaks resources (timers, goroutines) until the parent context is cancelled.
Use WithCancel for manual cancellation:
func (s *Supervisor) Run(ctx context.Context) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
g, ctx := errgroup.WithContext(ctx)
g.Go(func() error { return s.runWorkerA(ctx) })
g.Go(func() error { return s.runWorkerB(ctx) })
// If any worker returns an error, errgroup cancels ctx,
// which signals all other workers to stop.
return g.Wait()
}
Check context cancellation in loops:
// ✅ Good — respects cancellation
func processItems(ctx context.Context, items []Item) error {
for _, item := range items {
if err := ctx.Err(); err != nil {
return fmt.Errorf("processing cancelled: %w", err)
}
if err := process(ctx, item); err != nil {
return fmt.Errorf("process item %s: %w", item.ID, err)
}
}
return nil
}
// ❌ Bad — runs to completion even if cancelled
func processItems(ctx context.Context, items []Item) error {
for _, item := range items {
process(ctx, item) // ignores ctx cancellation between items
}
return nil
}
3. Timeouts and Deadlines
WithTimeout for duration-based limits:
func (c *Client) FetchUser(ctx context.Context, id string) (*User, error) {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, c.url+"/users/"+id, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch user %s: %w", id, err)
}
defer resp.Body.Close()
// ...
}
WithDeadline for absolute time limits:
// Use when coordinating with external deadlines (SLAs, cron windows)
deadline := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
ctx, cancel := context.WithDeadline(ctx, deadline)
defer cancel()
Timeout budgets — don't exceed parent timeout:
// ✅ Good — child timeout shorter than parent
func handler(ctx context.Context) error {
// Parent has 30s timeout (from HTTP server)
// Give DB query 5s of the 30s budget
dbCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
data, err := db.QueryContext(dbCtx, query)
// Give external API 10s of the remaining budget
apiCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
result, err := client.Call(apiCtx, data)
return nil
}
// ❌ Bad — child timeout exceeds parent (silently capped anyway)
ctx, cancel := context.WithTimeout(parentCtx, 60*time.Second) // parent has 5s left
// This timeout is 60s but will actually fire at parent's deadline
Check if deadline exists:
if deadline, ok := ctx.Deadline(); ok {
remaining := time.Until(deadline)
if remaining < minRequired {
return fmt.Errorf("insufficient time remaining: %v", remaining)
}
}
4. Context Values
Use sparingly — only for request-scoped metadata:
// ✅ Appropriate uses:
// - Request ID
// - Trace/span ID
// - Authenticated user info
// - Request-scoped logger
// ❌ Bad uses:
// - Database connections (use dependency injection)
// - Configuration (use struct fields)
// - Function parameters (pass explicitly)
Use unexported key types to prevent collisions:
// ✅ Good — unexported type prevents key collisions
type contextKey struct{}
var requestIDKey = contextKey{}
func WithRequestID(ctx context.Context, id string) context.Context {
return context.WithValue(ctx, requestIDKey, id)
}
func RequestID(ctx context.Context) string {
id, _ := ctx.Value(requestIDKey).(string)
return id
}
// ❌ Bad — string keys risk collisions across packages
ctx = context.WithValue(ctx, "request_id", id) // any package could overwrite this
Always provide accessor functions — never expose the key:
// ✅ Good — clean API with accessors
rid := middleware.RequestID(ctx)
// ❌ Bad — exposes internal key type
rid := ctx.Value(requestIDKey).(string) // caller needs key, risks panic on nil
5. Context in HTTP Handlers
Use r.Context() for the request context:
func (h *Handler) GetUser(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() // carries cancellation when client disconnects
user, err := h.service.GetUser(ctx, id)
if err != nil {
if errors.Is(err, context.Canceled) {
return // client disconnected, no point writing response
}
// handle error...
}
// ...
}
Attach values via middleware:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, err := authenticate(r)
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := WithUser(r.Context(), user)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
6. Context in Testing
Use context with timeout in tests to prevent hangs:
func TestSlowOperation(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
result, err := slowOperation(ctx)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// assert result...
}
Test cancellation behavior:
func TestCancellation(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
cancel() // cancel immediately
_, err := operation(ctx)
if !errors.Is(err, context.Canceled) {
t.Errorf("expected context.Canceled, got %v", err)
}
}
7. context.Background() vs context.TODO()
| Function | When to use |
|---|---|
context.Background() | Top-level: main(), init(), test setup. Intentional root context. |
context.TODO() | Placeholder when you don't know which context to use yet. Signals "this needs to be fixed". |
context.TODO() is a code smell in production code — replace it before shipping.
Verification Checklist
context.Contextis the first parameter in all functions that accept it- No context stored in struct fields
defer cancel()called immediately afterWithCancel,WithTimeout,WithDeadline- Long loops check
ctx.Err()between iterations - Child timeouts don't exceed parent timeout budget
- Context values use unexported key types with accessor functions
- Only request-scoped metadata stored in context values (not configs, connections)
- HTTP handlers use
r.Context()and pass it downstream - No
nilcontext passed — usecontext.TODO()orcontext.Background() - Tests use
context.WithTimeoutto prevent hanging
GitHub Repository
Frequently asked questions
What is the go-context skill?
go-context is a Claude Skill by eduardo-sl. Skills package instructions and resources that Claude loads on demand, so Claude can perform go-context-related tasks without extra prompting.
How do I install go-context?
Use the install commands on this page: add go-context to Claude Code as a plugin, or clone its repository into your skills directory, then restart Claude so it picks up the skill.
What category does go-context belong to?
go-context is in the Design category, tagged api and design.
Is go-context free to use?
Yes. go-context is listed on AIMCP and free to install.
Related Skills
Use the executing-plans skill when you have a complete implementation plan to execute in controlled batches with review checkpoints. It loads and critically reviews the plan, then executes tasks in small batches (default 3 tasks) while reporting progress between each batch for architect review. This ensures systematic implementation with built-in quality control checkpoints.
This skill dispatches a code-reviewer subagent to analyze code changes against requirements before proceeding. It should be used after completing tasks, implementing major features, or before merging to main. The review helps catch issues early by comparing the current implementation with the original plan.
This skill provides a comprehensive guide for developers to connect MCP servers to Claude Code using HTTP, stdio, or SSE transports. It covers installation, configuration, authentication, and security for integrating external services like GitHub, Notion, and custom APIs. Use it when setting up MCP integrations, configuring external tools, or working with Claude's Model Context Protocol.
This skill helps developers choose between Claude Code Web and CLI interfaces based on task analysis, then enables seamless session teleportation between these environments. It optimizes workflow by managing session state and context when switching between web, CLI, or mobile. Use it for complex projects requiring different tools at various stages.
