| name | golang-context-patterns |
| description | Context usage patterns for Go including cancellation, timeouts, deadlines, and database transactions. Use when handling HTTP requests, database operations, or implementing cancellation and timeout logic. |
Go Context Patterns
Context usage patterns for Go following 2025-2026 best practices.
Context Basics
Context carries:
- Cancellation signals
- Deadlines and timeouts
- Request-scoped values
Golden rule: Always pass context as first parameter.
func DoWork(ctx context.Context, data string) error {
}
HTTP Handler Context
Extract from request:
func (s *Server) HandleRequest(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
result, err := s.service.ProcessData(ctx, data)
if err != nil {
if errors.Is(err, context.Canceled) {
return
}
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
json.NewEncoder(w).Encode(result)
}
Database Transaction Context Pattern
Critical pattern for TARSy:
func (s *SessionService) CreateSession(ctx context.Context, req CreateSessionRequest) (*ent.AlertSession, error) {
writeCtx, cancel := context.WithTimeoutCause(ctx, 5*time.Second,
fmt.Errorf("create session %s: db write timed out", req.SessionID),
)
defer cancel()
tx, err := s.client.Tx(writeCtx)
if err != nil {
return nil, fmt.Errorf("failed to start transaction: %w", err)
}
defer func() { _ = tx.Rollback() }()
session, err := tx.AlertSession.Create().
SetID(req.SessionID).
Save(writeCtx)
if err != nil {
return nil, fmt.Errorf("failed to create session: %w", err)
}
if err := tx.Commit(); err != nil {
return nil, fmt.Errorf("failed to commit: %w", err)
}
return session, nil
}
Context strategy for database operations:
- By default, derive a bounded context from the request context to preserve cancellation and upstream deadlines:
context.WithTimeoutCause(httpCtx, 5*time.Second, cause)
- This ensures caller deadlines and cancellation signals propagate to the database layer
- Detach to
context.Background() only for explicitly designed background workflows (fire-and-forget patterns, queued jobs, cleanup routines) where upstream cancellation should be intentionally ignored
Context Timeout Patterns
WithTimeoutCause for operations with deadline:
func FetchData(ctx context.Context, url string) ([]byte, error) {
ctx, cancel := context.WithTimeoutCause(ctx, 10*time.Second,
fmt.Errorf("fetch %s timed out", url),
)
defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
WithDeadlineCause for specific time:
func ProcessByDeadline(ctx context.Context, deadline time.Time) error {
ctx, cancel := context.WithDeadlineCause(ctx, deadline,
fmt.Errorf("processing missed deadline %s", deadline),
)
defer cancel()
return doWork(ctx)
}
Context Cancellation
WithCancelCause for manual cancellation:
func ProcessWithCancel(ctx context.Context) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
go func() {
if err := backgroundWork(ctx); err != nil {
cancel(fmt.Errorf("background work failed: %w", err))
}
}()
return mainWork(ctx)
}
Checking for cancellation:
func LongOperation(ctx context.Context) error {
for i := range 1000 {
select {
case <-ctx.Done():
return context.Cause(ctx)
default:
}
processItem(i)
}
return nil
}
Database Query Context
Using context for queries:
func (s *SessionService) GetSession(ctx context.Context, id string) (*ent.AlertSession, error) {
session, err := s.client.AlertSession.
Query().
Where(alertsession.IDEQ(id)).
Only(ctx)
if err != nil {
return nil, err
}
return session, nil
}
Query with timeout:
func (s *SessionService) ListSessions(ctx context.Context, limit int) ([]*ent.AlertSession, error) {
queryCtx, cancel := context.WithTimeoutCause(ctx, 3*time.Second,
fmt.Errorf("list sessions query timed out"),
)
defer cancel()
sessions, err := s.client.AlertSession.
Query().
Limit(limit).
All(queryCtx)
if err != nil {
return nil, err
}
return sessions, nil
}
Goroutine Context Propagation
Pass context to goroutines:
func ProcessConcurrently(ctx context.Context, items []string) error {
errChan := make(chan error, len(items))
for _, item := range items {
go func() {
errChan <- processItem(ctx, item)
}()
}
for range items {
if err := <-errChan; err != nil {
return err
}
}
return nil
}
Cancelling goroutines on error:
func ProcessWithEarlyExit(ctx context.Context, items []string) error {
ctx, cancel := context.WithCancelCause(ctx)
defer cancel(nil)
errChan := make(chan error, len(items))
for _, item := range items {
go func() {
err := processItem(ctx, item)
if err != nil {
cancel(err)
}
errChan <- err
}()
}
for range items {
if err := <-errChan; err != nil {
return err
}
}
return nil
}
Context AfterFunc
Run cleanup when context is cancelled:
func WatchResource(ctx context.Context, res *Resource) {
stop := context.AfterFunc(ctx, func() {
res.Release()
})
defer stop()
}
Context Best Practices
DO:
- Always pass context as first parameter
- Use
context.Background() as root context only at program boundaries (main, init, top-level background jobs)
- Use
WithTimeoutCause/WithCancelCause/WithDeadlineCause — always attach a cause
- Use
context.Cause(ctx) to retrieve the cause on cancellation
- Derive bounded contexts from the caller's context for request-scoped operations (including DB writes)
- Check
ctx.Done() in long-running loops
- Propagate context through call chains
DON'T:
- Store context in structs (pass as parameter instead)
- Pass
nil context (use context.TODO() if unsure)
- Use context for optional function parameters
- Create new background context when you have a valid parent context
- Ignore context cancellation errors
TARSy-Specific Patterns
HTTP → Service → Database:
func (h *Handler) CreateSession(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
session, err := h.sessionService.CreateSession(ctx, req)
}
func (s *SessionService) CreateSession(ctx context.Context, req CreateSessionRequest) (*ent.AlertSession, error) {
writeCtx, cancel := context.WithTimeoutCause(ctx, 5*time.Second,
fmt.Errorf("create session: db write timed out"),
)
defer cancel()
tx, err := s.client.Tx(writeCtx)
}
Background job context:
func (s *SessionService) CleanupOldSessions(ctx context.Context) error {
cleanupCtx, cancel := context.WithTimeoutCause(
context.Background(), 30*time.Second,
fmt.Errorf("session cleanup timed out"),
)
defer cancel()
count, err := s.SoftDeleteOldSessions(cleanupCtx, 90)
if err != nil {
return fmt.Errorf("cleanup failed: %w", err)
}
log.Printf("Cleaned up %d sessions", count)
return nil
}
Transaction Context Guidelines
Standard pattern for request-scoped writes:
func (s *Service) WriteOperation(ctx context.Context, data Data) error {
writeCtx, cancel := context.WithTimeoutCause(ctx, 5*time.Second,
fmt.Errorf("write operation: db timed out"),
)
defer cancel()
tx, err := s.client.Tx(writeCtx)
if err != nil {
return fmt.Errorf("failed to start transaction: %w", err)
}
defer func() { _ = tx.Rollback() }()
result, err := tx.Entity.Create().
SetData(data).
Save(writeCtx)
if err != nil {
return err
}
if err := tx.Commit(); err != nil {
return err
}
return nil
}
Quick Reference
Context creation:
context.Background()
context.TODO()
context.WithCancelCause(parent)
context.WithTimeoutCause(parent, d, err)
context.WithDeadlineCause(parent, t, err)
context.AfterFunc(ctx, fn)
Context checking:
<-ctx.Done()
context.Cause(ctx)
errors.Is(err, context.Canceled)
errors.Is(err, context.DeadlineExceeded)
Common timeouts:
- HTTP requests: 10-30 seconds
- Database queries: 3-5 seconds
- Database writes: 5-10 seconds
- Background jobs: 30-60 seconds