Go

Which newer context features matter for concurrent code: WithCancelCause, AfterFunc, WithoutCancel?

Question 249MediumGo 1.22 to 1.25
  • WithCancelCause (1.20): cancel(err) records why the context was cancelled, and context.Cause(ctx) returns that error while ctx.Err() is still Canceled. It makes logs from fan-out work far more useful. WithTimeoutCause and WithDeadlineCause arrived in 1.21.
  • AfterFunc (1.21): runs a function in its own goroutine when the context is done, and returns a stop func. It replaces the hand-written go func(){ <-ctx.Done(); ... }() goroutine that leaks when the context is never cancelled. It is handy for waking a sync.Cond or closing a connection on cancel.
  • WithoutCancel (1.21): keeps the context's values but drops its cancellation. Use it for work that must outlive the request, such as audit logs or the singleflight body.
ctx, cancel := context.WithCancelCause(parent)
defer cancel(nil)

go func() {
	if err := watchQuota(ctx); err != nil {
		cancel(fmt.Errorf("quota exceeded: %w", err))
	}
}()

stop := context.AfterFunc(ctx, func() { conn.Close() }) // unblock reads on cancel
defer stop()

<-ctx.Done()
log.Println(ctx.Err(), context.Cause(ctx)) // context canceled quota exceeded: ...

Rules the interviewer checks:

  • ctx is the first parameter.
  • Never store a ctx in a struct.
  • Always call cancel; go vet's lostcancel check catches misses.
  • Context values are for request-scoped data, not for optional parameters.

More on Concurrency Patterns & sync

All 38 Concurrency Patterns & sync questions