skip to content

When several Go channels are merged into one, what ordering does the merged channel guarantee?

level: middleimportance: should knowfreq 46%

answer

  1. two orderings, not one
  2. per stream, then across streams
  3. one forwarder receives sequentially
  4. the runtime picks the interleaving
  5. no round-robin, no timestamp sort

basics

~20 s

Values from any one source keep that source's order, because a single goroutine forwards it. Across sources nothing is guaranteed: the merged channel interleaves arbitrarily, with no round-robin, no fairness and no ordering by timestamp.

solid answer

~50 s

A fan-in merge preserves order **within** each source and guarantees nothing **between** sources. Each source has one forwarding goroutine that receives and re-sends sequentially, so quotes from provider A reach the consumer in A's order. But which of the several blocked forwarders gets to complete its send next is decided by the runtime, not by arrival time, and the language specification promises no fairness at all. A `select` over ready cases picks uniformly at random by design, precisely so nobody can rely on a source ordering. Practically that means a fast feed can dominate the merged stream, and the merged output is not sorted by the timestamp carried inside the values — you cannot recover a global order after merging. If you need one, you must hold a pending value from every source and emit the smallest yourself, which costs you the latency of the slowest source.

code

go · 9 lines
go
a := make(chan string)
b := make(chan string)
// a produces "a1" then "a2"; b produces "b1" then "b2"
merged := merge(a, b)
for v := range merged {
	// a1 always arrives before a2, and b1 before b2.
	// How the two streams interleave is not specified.
	fmt.Println(v)
}

go deeper

for a junior

Remember the short version: each input keeps its own order, and how the inputs mix together is not defined. Do not promise an interviewer that values arrive in the order they were produced.

for a middle

Explain the mechanism: one forwarder per source receives sequentially, so per-source order holds, while the choice of which blocked sender proceeds is the runtime's, and a select among ready cases is randomised on purpose.

for a senior

Show the production consequence: self-describing values with sequence numbers, property-based assertions in tests, and an explicit ordering stage with a watermark when the business really needs time order.

for a principal

Own the contract you publish. Decide whether the merged stream is documented as unordered and consumers must tolerate it, or whether you pay slowest-source latency for ordering — and make that choice explicit rather than accidental.

## Two different questions hide inside "ordering" When a merge combines N inputs into one output there are two orderings to reason about, and candidates routinely conflate them. **Per-source order.** Are values from one particular input still in their original relative order once merged? **Yes.** In the goroutine-per-source shape, exactly one goroutine receives from a given input, and it does so sequentially: it takes one value, sends it on, and only then receives the next. A channel is a FIFO queue, so the forwarder sees the source's values in order, and it emits them in the same order. Nothing in the merge can reorder a single source's stream. **Cross-source order.** Is the merged sequence in any way related to the order values arrived at their respective sources? **No.** This is the part that has no guarantee. ## Why cross-source order is undefined Each forwarder is an independent goroutine. When several are blocked trying to send into the same unbuffered output, exactly one of them completes when the consumer receives — and which one is a scheduling decision. The Go memory model and the language specification do not order goroutines that are not synchronised with each other, so "the value produced earliest wins" is not a property you can lean on. The runtime does maintain internal queues of waiting senders, but that is an implementation detail of one compiler, not a contract; code that depends on it is code that breaks on a different release or under a different core count. The single-goroutine `select` variant is even more explicit: **when more than one case of a `select` is ready, one is chosen uniformly at random.** That randomisation exists exactly so that programs cannot come to depend on case order. ## Fairness and starvation There is no round-robin. If one market-data provider streams ten times faster than the others, roughly ten times as many of its quotes appear in the merged stream — the merge does not equalise rates. Nor does it drop anything: every value that a source sends is eventually forwarded, because its forwarder stays blocked on that send until it succeeds. So slow sources are not silenced, they are simply outnumbered. If you need a share guarantee — say, at most one quote per provider per round — the merge cannot give it to you. You have to write it: receive from each source into a small per-source holding area and emit deliberately, which is a different pattern with different blocking behaviour. ## Timestamps do not save you A very common wrong answer is "the values carry timestamps, so the merged stream is in time order". It is not, for two independent reasons. First, the merge itself introduces reordering: a value produced at t=1 on a slow provider can be forwarded after a value produced at t=2 on a fast one, purely because of when each forwarder got scheduled. Second, the timestamps come from different machines. Clocks across independent providers are skewed, so even a perfectly ordered merge would not give you a true global order of events. To emit in timestamp order you must have at least one pending value from **every** live source before you can safely emit the smallest one — otherwise a later arrival from a silent source could belong earlier. That is a fundamentally different pattern: it blocks on the slowest source, it needs a policy for a source that goes quiet (a timeout, a watermark, or dropping the laggard), and it buys ordering at the cost of latency. Fan-in gives you none of that machinery, and pretending otherwise is how ordering bugs reach production. ## What you can rely on Three things, and no more: every value sent on a source before that source is closed is eventually delivered exactly once to the consumer; each source's values appear in their own order; and the merged channel closes only after every source has been drained and closed. Design the consumer so those three facts are enough — for example, make each value self-describing (which provider, which instrument, which sequence number) rather than inferring anything from position in the merged stream. ## A practical consequence for tests A test that asserts the exact merged sequence of a multi-source merge is flaky by construction. Assert on the multiset of values received, on the per-source subsequences, and on the fact that the merged channel closed — never on the interleaving.

  • One provider produces ten times faster than the others. Does the merge starve the slow ones?
    It does not silence them, but it does not balance either. Each forwarder stays blocked on its send until that send completes, so every value is eventually delivered; the fast provider simply contributes proportionally more of the merged stream. If you need a per-source share, you have to build it — the merge offers no fairness knob.
  • How would you produce a merged stream ordered by the timestamp inside each quote?
    Not with a plain fan-in. You need a pending value held from every live source so you can safely emit the smallest, which means waiting for the slowest source and adding a policy for one that goes quiet — a watermark, a timeout, or dropping late arrivals. Ordering costs latency, and cross-provider clock skew limits how meaningful the result is.
  • How should a test assert on the output of a merge over three sources?
    On properties, never on the exact sequence. Check that the multiset of received values matches what was sent, that each source's own values appear in their original relative order, and that the merged channel eventually closed. Asserting a specific interleaving produces a test that passes locally and fails under load or on a different core count.

saying these in an interview costs you the question

  • Says the merged channel round-robins between sources
  • Claims a select picks the case that became ready first
  • Assumes merged values arrive sorted by their timestamps
  • Thinks the merge can reorder a single source's values
  • Expects fairness because goroutines are preemptible