Go

How should a long-running worker loop respect cancellation? Discuss select fairness and CPU-bound loops.

Question 281MediumGo 1.22 to 1.25

For channel-driven loops, put ctx.Done() in the same select as the work channel. For CPU-bound loops that never block, check ctx.Err() regularly. Checking every N iterations keeps the overhead negligible: Err() is cheap, but it is a synchronized read (historically a mutex) that you don't want on every iteration of a hot loop.

func consume(ctx context.Context, jobs <-chan Job) error {
	for {
		select {
		case <-ctx.Done():
			return context.Cause(ctx)
		case j, ok := <-jobs:
			if !ok {
				return nil
			}
			if err := ctx.Err(); err != nil { // priority check: see below
				return err
			}
			handle(ctx, j)
		}
	}
}

func crunch(ctx context.Context, data []float64) (float64, error) {
	var sum float64
	for i := range len(data) {
		if i%4096 == 0 {
			if err := ctx.Err(); err != nil {
				return 0, err
			}
		}
		sum += math.Sqrt(data[i])
	}
	return sum, nil
}

Fairness gotcha: when several select cases are ready, Go picks one at random. With a busy jobs channel and a canceled context, the loop can keep taking jobs for several more iterations. If cancellation must win, re-check ctx.Err() after receiving, as above.

Also, never use select { case <-ctx.Done(): ... default: } as a spin loop without doing work in it. That just burns CPU.

More on Context

All 35 Context questions