When Simple Context Isn’t Enough
By now you’ve got the basics down, but real-world systems throw curveballs that basic context patterns can’t handle. What do you do when you need different parts of an operation to have different timeout behaviors? How do you coordinate partial failures across multiple services? These are the problems that advanced context patterns solve.
I’ve spent years dealing with these edge cases, and I’ve learned that the most elegant solutions often involve combining multiple context concepts in creative ways.
Context Multiplexing (Different Rules for Different Operations)
Sometimes you need to run operations in parallel, but each one needs different cancellation and timeout rules:
type ContextMultiplexer struct {
parent context.Context
children map[string]contextInfo
mu sync.RWMutex
}
type contextInfo struct {
ctx context.Context
cancel context.CancelFunc
}
func NewContextMultiplexer(parent context.Context) *ContextMultiplexer {
return &ContextMultiplexer{
parent: parent,
children: make(map[string]contextInfo),
}
}
func (cm *ContextMultiplexer) CreateChild(name string, timeout time.Duration) context.Context {
cm.mu.Lock()
defer cm.mu.Unlock()
ctx, cancel := context.WithTimeout(cm.parent, timeout)
cm.children[name] = contextInfo{ctx: ctx, cancel: cancel}
return ctx
}
func (cm *ContextMultiplexer) CancelChild(name string) {
cm.mu.Lock()
defer cm.mu.Unlock()
if info, exists := cm.children[name]; exists {
info.cancel()
delete(cm.children, name)
}
}
func (cm *ContextMultiplexer) CancelAll() {
cm.mu.Lock()
defer cm.mu.Unlock()
for _, info := range cm.children {
info.cancel()
}
cm.children = make(map[string]contextInfo)
}
// Real-world usage: fetching user data from multiple sources
func fetchCompleteUserProfile(ctx context.Context, userID string) (*UserProfile, error) {
mux := NewContextMultiplexer(ctx)
defer mux.CancelAll()
// Different timeouts for different data sources
profileCtx := mux.CreateChild("profile", 2*time.Second) // Fast
prefsCtx := mux.CreateChild("preferences", 5*time.Second) // Medium
historyCtx := mux.CreateChild("history", 10*time.Second) // Slow
type fetchResult struct {
name string
data interface{}
err error
}
results := make(chan fetchResult, 3)
go func() {
data, err := fetchBasicProfile(profileCtx, userID)
results <- fetchResult{"profile", data, err}
}()
go func() {
data, err := fetchUserPreferences(prefsCtx, userID)
results <- fetchResult{"preferences", data, err}
}()
go func() {
data, err := fetchUserHistory(historyCtx, userID)
results <- fetchResult{"history", data, err}
}()
profile := &UserProfile{}
for i := 0; i < 3; i++ {
select {
case result := <-results:
if result.err != nil {
// Cancel everything on any error
mux.CancelAll()
return nil, fmt.Errorf("%s fetch failed: %w", result.name, result.err)
}
// Populate profile based on result type...
case <-ctx.Done():
return nil, ctx.Err()
}
}
return profile, nil
}
This pattern gives you fine-grained control over each operation while maintaining overall coordination.
Dynamic Context Adaptation
Sometimes you need context behavior to change based on runtime conditions. Here’s how I handle that:
type AdaptiveContext struct {
base context.Context
modifiers []ContextModifier
}
type ContextModifier interface {
ShouldApply(ctx context.Context) bool
Apply(ctx context.Context) (context.Context, context.CancelFunc)
}
// Example: Extend timeout for premium users
type PremiumUserModifier struct {
extraTime time.Duration
}
func (pum *PremiumUserModifier) ShouldApply(ctx context.Context) bool {
userID, ok := GetUserID(ctx)
return ok && isPremiumUser(userID)
}
func (pum *PremiumUserModifier) Apply(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, pum.extraTime)
}
// Example: Reduce timeout under high load
type LoadBasedModifier struct {
reducedTimeout time.Duration
}
func (lbm *LoadBasedModifier) ShouldApply(ctx context.Context) bool {
return getCurrentSystemLoad() > 0.8
}
func (lbm *LoadBasedModifier) Apply(ctx context.Context) (context.Context, context.CancelFunc) {
return context.WithTimeout(ctx, lbm.reducedTimeout)
}
func NewAdaptiveContext(base context.Context) *AdaptiveContext {
return &AdaptiveContext{
base: base,
modifiers: make([]ContextModifier, 0),
}
}
func (ac *AdaptiveContext) AddModifier(modifier ContextModifier) {
ac.modifiers = append(ac.modifiers, modifier)
}
func (ac *AdaptiveContext) CreateContext() (context.Context, context.CancelFunc) {
ctx := ac.base
var cancels []context.CancelFunc
for _, modifier := range ac.modifiers {
if modifier.ShouldApply(ctx) {
var cancel context.CancelFunc
ctx, cancel = modifier.Apply(ctx)
if cancel != nil {
cancels = append(cancels, cancel)
}
}
}
// Return combined cancel function
return ctx, func() {
for _, cancel := range cancels {
cancel()
}
}
}
This lets your context behavior adapt to user types, system load, or any other runtime conditions.
Context Pipelines (Chaining Operations with Context Evolution)
In complex processing pipelines, each stage might need to modify the context for subsequent stages:
type PipelineStage interface {
Process(ctx context.Context, data interface{}) (context.Context, interface{}, error)
Name() string
}
type ValidationStage struct{}
func (vs *ValidationStage) Name() string { return "validation" }
func (vs *ValidationStage) Process(ctx context.Context, data interface{}) (context.Context, interface{}, error) {
req := data.(*ProcessingRequest)
// High priority requests get extended timeouts
if req.Priority == "high" {
ctx = context.WithValue(ctx, "priority", "high")
// Extend timeout for high priority
newCtx, _ := context.WithTimeout(ctx, 60*time.Second)
ctx = newCtx
}
if err := validateRequest(req); err != nil {
return ctx, nil, err
}
return ctx, req, nil
}
type ProcessingStage struct{}
func (ps *ProcessingStage) Name() string { return "processing" }
func (ps *ProcessingStage) Process(ctx context.Context, data interface{}) (context.Context, interface{}, error) {
req := data.(*ProcessingRequest)
// Check if previous stage marked this as high priority
if priority := ctx.Value("priority"); priority == "high" {
result, err := processHighPriority(ctx, req)
return ctx, result, err
}
result, err := processNormal(ctx, req)
return ctx, result, err
}
type ContextPipeline struct {
stages []PipelineStage
}
func NewContextPipeline(stages ...PipelineStage) *ContextPipeline {
return &ContextPipeline{stages: stages}
}
func (cp *ContextPipeline) Execute(ctx context.Context, initialData interface{}) (interface{}, error) {
currentCtx := ctx
currentData := initialData
for _, stage := range cp.stages {
var err error
currentCtx, currentData, err = stage.Process(currentCtx, currentData)
if err != nil {
return nil, fmt.Errorf("stage %s failed: %w", stage.Name(), err)
}
// Check for cancellation between stages
select {
case <-currentCtx.Done():
return nil, currentCtx.Err()
default:
}
}
return currentData, nil
}
This pipeline pattern allows each stage to influence how subsequent stages behave through context modification.
Context Resource Pooling
For expensive resources that need context-aware lifecycle management:
type ContextAwarePool struct {
pool sync.Pool
active map[interface{}]context.CancelFunc
mu sync.RWMutex
maxAge time.Duration
}
func NewContextAwarePool(factory func() interface{}, maxAge time.Duration) *ContextAwarePool {
return &ContextAwarePool{
pool: sync.Pool{New: factory},
active: make(map[interface{}]context.CancelFunc),
maxAge: maxAge,
}
}
func (cap *ContextAwarePool) Get(ctx context.Context) (interface{}, error) {
resource := cap.pool.Get()
// Create context for this resource with max age
resourceCtx, cancel := context.WithTimeout(ctx, cap.maxAge)
cap.mu.Lock()
cap.active[resource] = cancel
cap.mu.Unlock()
// Monitor for context cancellation
go func() {
<-resourceCtx.Done()
cap.forceReturn(resource)
}()
return resource, nil
}
func (cap *ContextAwarePool) Put(resource interface{}) {
cap.mu.Lock()
if cancel, exists := cap.active[resource]; exists {
cancel()
delete(cap.active, resource)
}
cap.mu.Unlock()
cap.pool.Put(resource)
}
func (cap *ContextAwarePool) forceReturn(resource interface{}) {
cap.mu.Lock()
if cancel, exists := cap.active[resource]; exists {
cancel()
delete(cap.active, resource)
}
cap.mu.Unlock()
// Clean up the resource if needed
if closer, ok := resource.(io.Closer); ok {
closer.Close()
}
}
This pool automatically manages resource lifecycles based on context cancellation and age limits.
Context Merging (Combining Multiple Contexts)
When you need to combine contexts from different sources while preserving all their capabilities:
type MergedContext struct {
contexts []context.Context
done chan struct{}
err error
once sync.Once
}
func MergeContexts(contexts ...context.Context) *MergedContext {
mc := &MergedContext{
contexts: contexts,
done: make(chan struct{}),
}
go mc.monitor()
return mc
}
func (mc *MergedContext) monitor() {
// Use reflection to wait on multiple channels
cases := make([]reflect.SelectCase, len(mc.contexts))
for i, ctx := range mc.contexts {
cases[i] = reflect.SelectCase{
Dir: reflect.SelectRecv,
Chan: reflect.ValueOf(ctx.Done()),
}
}
chosen, _, _ := reflect.Select(cases)
mc.once.Do(func() {
mc.err = mc.contexts[chosen].Err()
close(mc.done)
})
}
func (mc *MergedContext) Done() <-chan struct{} {
return mc.done
}
func (mc *MergedContext) Err() error {
return mc.err
}
func (mc *MergedContext) Deadline() (time.Time, bool) {
var earliest time.Time
hasDeadline := false
for _, ctx := range mc.contexts {
if deadline, ok := ctx.Deadline(); ok {
if !hasDeadline || deadline.Before(earliest) {
earliest = deadline
hasDeadline = true
}
}
}
return earliest, hasDeadline
}
func (mc *MergedContext) Value(key interface{}) interface{} {
for _, ctx := range mc.contexts {
if value := ctx.Value(key); value != nil {
return value
}
}
return nil
}
This merged context cancels when any of its constituent contexts cancel, and uses the earliest deadline.
These advanced patterns become essential when you’re building complex distributed systems where simple request-response patterns aren’t enough. They give you the tools to coordinate sophisticated operations while maintaining the benefits of context-based lifecycle management.
Next, we’ll dive into error handling and recovery patterns that work with these advanced context scenarios.