Go

How would you merge two contexts so the result is canceled when either one is?

Question 283HardGo 1.22 to 1.25

The standard library has no merge function, but WithCancelCause plus AfterFunc makes one cheap, without a dedicated watcher goroutine when both inputs are stdlib contexts. A typical need: a request context combined with a server shutdown context, or a stream's context combined with a per-message context.

// Merge returns a context canceled when a or b is done.
// Values and Deadline come from a only.
func Merge(a, b context.Context) (context.Context, context.CancelFunc) {
	ctx, cancel := context.WithCancelCause(a)
	stop := context.AfterFunc(b, func() {
		cancel(context.Cause(b))
	})
	return ctx, func() {
		stop()
		cancel(context.Canceled)
	}
}

func main() {
	a, cancelA := context.WithCancel(context.Background())
	defer cancelA()
	b, cancelB := context.WithTimeout(context.Background(), 10*time.Millisecond)
	defer cancelB()

	m, cancel := Merge(a, b)
	defer cancel()
	<-m.Done()
	fmt.Println(m.Err(), "|", context.Cause(m)) // context canceled | context deadline exceeded
}

Discussion points:

  • Err() is Canceled because we canceled manually. Cause keeps the real reason from b.
  • Deadline() reports only a's deadline. If that matters, compute the earlier of the two and wrap the result in WithDeadline.
  • Value lookups see only a. Merging values is ambiguous, which is one reason the stdlib does not provide merging.
  • The returned cancel must call stop(), or the callback stays registered on b.

More on Context

All 35 Context questions