Skip to content

select chooses at random

Level: 201 · anyone who wrote the important case first and expected it to win

One line: When more than one case of a select can proceed, Go picks one of them at random, with every ready case equally likely, so the order of the cases is not a priority. A priority is something you write: a non-blocking check of the urgent channel, before the select that waits on everything.

Verified output of select_chooses_at_random_go.go — regenerated by tools/run_examples.py, never hand-typed.

1. Two cases, both ready on every one of 10000 selects
   urgent chosen between 40% and 60% of the time:  true
   routine chosen between 40% and 60% of the time: true

2. 100 urgent and 100 routine messages already queued
   plain select handled every urgent message first:        false
   urgent-first select handled every urgent message first: true

Reading the output

1. Writing a case first does not make it win. Both channels held a message before every one of the 10,000 selects, and the urgent case was written first. It was chosen between 40% and 60% of the time, and so was routine. The Go specification ↗ says that when one or more communications can proceed, a single one is chosen "via a uniform pseudo-random selection". The runtime does this by shuffling the cases into a new random order each time a select runs. In runtime/select.go, that is the loop under the comment generate permuted order.

Why a threshold and not the counts. The counts change on every run:

Real runs — go1.25.5, x86-64 Mac, macOS 26, 10 runs, 2026-09-14
urgent 5003  routine 4997
urgent 4999  routine 5001
urgent 4965  routine 5035
urgent 5065  routine 4935
urgent 4967  routine 5033
urgent 4989  routine 5011
urgent 4992  routine 5008
urgent 4978  routine 5022
urgent 4992  routine 5008
urgent 5036  routine 4964

If every select is a fair coin toss between the two cases, the number of urgent picks out of 10,000 has a mean of 5,000 and a standard deviation of 50. To fall outside 4,000–6,000, a count must land 20 standard deviations from the mean. The exact binomial probability of that is about 1.2 × 10−89, so the key cannot fail by chance on any machine where select really does choose uniformly. The key also catches the mistake it is about: a select that always took the first ready case would print false on both lines.

To see the counts on your own machine, run bash demo/tally.sh from this folder. It builds demo/tally_go.go and runs it ten times; give it a number for more runs.

2. Priority is something you write. Before either loop, 100 urgent and 100 routine messages were queued, and nothing added more. The plain select interleaved them, so at least one routine message came before the last urgent one. For the plain select to take every urgent message first by chance, it would have had to pick urgent 100 times in a row while both channels were ready, a probability of 2−100, about 7.9 × 10−31. urgentThenAny first looks at urgent alone, with a default so that looking does not wait. Only when urgent is empty does it go on to a select that waits on both. It handled all 100 urgent messages before any routine one.

What that priority does not promise. The inner select still chooses at random among the cases that are ready when it runs. Suppose it is waiting on both channels, and an urgent and a routine message arrive at almost the same moment: either can be taken first. Part 1 is that case. What the outer check does promise is that urgent work that was already waiting is never passed over for routine work. That is usually what a priority needs, for example a worker that checks whether it has been cancelled before it takes another job (Cancel reaches every goroutine).

select_chooses_at_random_go.go

select_chooses_at_random_go.go in full — pasted here by tools/run_examples.py from the file CI runs.

// When several cases of a select are ready, one is chosen at random: the order
// the cases are written in is not a priority. Priority is something you write.
//
//  go run select_chooses_at_random_go.go
package main

import "fmt"

const selects = 10_000

func main() {
    fmt.Println("1. Two cases, both ready on every one of", selects, "selects")
    urgent := make(chan string, 1)
    routine := make(chan string, 1)
    fromUrgent, fromRoutine := 0, 0
    for range selects {
        if len(urgent) == 0 {
            urgent <- "disk almost full"
        }
        if len(routine) == 0 {
            routine <- "rotate the logs"
        }
        select {
        case <-urgent: // written first
            fromUrgent++
        case <-routine:
            fromRoutine++
        }
    }
    fmt.Println("   urgent chosen between 40% and 60% of the time: ", between40and60(fromUrgent))
    fmt.Println("   routine chosen between 40% and 60% of the time:", between40and60(fromRoutine))

    fmt.Println()
    fmt.Println("2. 100 urgent and 100 routine messages already queued")
    urgentQueue, routineQueue := queued(100)
    var plain []string
    for range 200 {
        plain = append(plain, plainSelect(urgentQueue, routineQueue))
    }
    fmt.Println("   plain select handled every urgent message first:       ", urgentFirst(plain))

    urgentQueue, routineQueue = queued(100)
    var prioritized []string
    for range 200 {
        prioritized = append(prioritized, urgentThenAny(urgentQueue, routineQueue))
    }
    fmt.Println("   urgent-first select handled every urgent message first:", urgentFirst(prioritized))
}

// plainSelect takes whichever message the select picks.
func plainSelect(urgent, routine <-chan string) string {
    select {
    case <-urgent:
        return "urgent"
    case <-routine:
        return "routine"
    }
}

// urgentThenAny looks at urgent alone first, without waiting, and only when it
// is empty waits on both.
func urgentThenAny(urgent, routine <-chan string) string {
    select {
    case <-urgent:
        return "urgent"
    default:
        select {
        case <-urgent:
            return "urgent"
        case <-routine:
            return "routine"
        }
    }
}

func between40and60(count int) bool {
    return count >= selects*40/100 && count <= selects*60/100
}

// queued returns two buffered channels holding n messages each.
func queued(n int) (urgent, routine chan string) {
    urgent = make(chan string, n)
    routine = make(chan string, n)
    for range n {
        urgent <- "disk almost full"
        routine <- "rotate the logs"
    }
    return urgent, routine
}

// urgentFirst reports whether no routine message came before the last urgent one.
func urgentFirst(handled []string) bool {
    seenRoutine := false
    for _, kind := range handled {
        if kind == "routine" {
            seenRoutine = true
        } else if seenRoutine {
            return false
        }
    }
    return true
}

What to do

  • Never rely on the order of cases. Write them in whatever order reads best, because the runtime ignores that order.
  • For a priority, check the urgent channel first with a non-blocking select, then wait on everything. The same shape puts <-ctx.Done() first in a worker loop, so a cancelled worker does not start one more job.
  • Count on the fairness. Because the choice is random, a channel that is always ready cannot starve another channel that is also ready. Part 1 shows each of the two getting about half of the selects.
  • Test a random choice with a threshold so wide that missing it by chance is out of the question, never with the counts themselves.

In other languages

  • The Rust library's Channels ↗ page uses std::sync::mpsc, which has no select at all. The crates that add one agree with Go about fairness. crossbeam_channel::select! picks a random operation when several are ready. tokio::select! picks a random branch to check first by default. Unlike Go, it also has a biased; mode, which checks branches from top to bottom and leaves fairness to you.
  • The Concurrency library's chapter 05, Message passing, which covers channels and select across languages, is planned.

Sources

  • The Go specification, Select statements ↗.
  • runtime/select.go in Go 1.25.5, where selectgo builds its random polling order.
  • Katherine Cox-Buday, Concurrency in Go (O'Reilly, 2017), chapter 3, "Go's Concurrency Building Blocks", the section on the select statement, which counts the choices between two ready channels.
  • Burak Serdar, Effective Concurrency in Go (Packt, 2023), chapter 2, "Go Concurrency Primitives", the section on channels.