reflect.Select over cases known at run time¶
Level: 301 · anyone merging a number of channels that a config file, not the source, decides
One line: A select statement names its cases in the source; reflect.Select takes a []SelectCase built in a loop, blocks like the statement, and returns the index it chose, the received Value and recvOK — a nil or zero Chan switches a case off, SelectDefault makes it non-blocking, and each call allocates where the statement does not.
Four sensors, each a chan int, are put into a slice of reflect.SelectCase with Dir: SelectRecv. The program fills one channel at a time and checks that chosen is that channel's index; fills all four and sorts what it receives, because which comes first is random; adds a default case and calls with nothing ready; closes a sensor and reads the zero value back with recvOK == false; switches that case off two ways; sends through a SelectSend case; and counts allocations per call against a two-case select statement.
Verified output of reflect_select_over_cases_known_at_run_time_go.go — regenerated by tools/run_examples.py, never hand-typed.
sensor 0 ready: chosen 0, value 20, recvOK true
sensor 1 ready: chosen 1, value 21, recvOK true
sensor 2 ready: chosen 2, value 22, recvOK true
sensor 3 ready: chosen 3, value 23, recvOK true
all four ready: received [30 31 32 33]
nothing ready: chosen 4 (the default), recv.IsValid false, recvOK false
sensor 2 closed: chosen 2, value 0, recvOK false
sensor 2 nil: chosen 4 (the default)
sensor 2 zero: chosen 4 (the default)
send case: chosen 0, recv.IsValid false, recvOK false, delivered "sensor 2 offline"
allocations per call, select statement, 2 cases: 0
allocations per call, reflect.Select, 2 cases: 2
Reading the output¶
chosen is the index of the case that ran. With one channel ready at a time, reflect.Select returns that channel's position in the slice, its value as a reflect.Value (recv.Int() reads it) and recvOK == true because a real send produced the value. The docs ↗ describe the function as a select statement: it blocks until at least one case can proceed, chooses uniformly at random among the ready ones, and executes exactly that one. That randomness is why the "all four ready" line prints a sorted slice rather than the order of arrival — the same rule as select chooses at random.
A default case returns its own index and no value. withDefault appends {Dir: SelectDefault} as case 4. With nothing ready, chosen is 4, recv is the zero Value (IsValid false) and recvOK is false. This is the non-blocking select of select waits on many channels, with the default case appended rather than written last.
A closed channel is always ready, with recvOK false. After close(sensors[2]), reflect.Select(cases) picks case 2 immediately and hands back 0 — the zero value of the channel's element type — with recvOK == false, exactly as v, ok := <-ch would (Closing a channel ends a range). A loop that leaves the closed case in the slice would spin on it for ever, which is why the next two lines matter.
Switching a case off: a nil channel or a zero Chan. Setting cases[2].Chan to a reflect.Value of a nil chan int makes that case one that can never proceed — the rule of A nil channel disables a case, and the default is chosen. Setting Chan to the zero reflect.Value{} does the same by a different route: the SelectCase docs ↗ say a receive case whose Chan is the zero Value is ignored. The zero Value is the cleaner switch for a slice built at run time, because it needs no channel type; the kata uses it.
A send case carries its value in Send. {Dir: SelectSend, Chan: ValueOf(alerts), Send: ValueOf("sensor 2 offline")} is chosen when the buffered alerts has room; recv is invalid and recvOK false because nothing was received, and the string comes out of the channel afterwards. A Send on a receive case, a value of the wrong type, or a receive-only channel in a send case are run-time panics from reflect.Select itself.
Two allocations per call. The select statement over two channels allocates nothing; reflect.Select over the same two channels allocates twice per call on Go 1.25 — the runtime's copy of the case list and the box for the received value. Reflection costs allocations puts the same measurement beside a benchmark. The docs also set a ceiling: Select supports a maximum of 65536 cases.
reflect_select_over_cases_known_at_run_time_go.go
reflect_select_over_cases_known_at_run_time_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// reflect.Select is a select statement whose cases are a slice built at run
// time. It returns the index of the case it took, the received Value, and
// whether that value came from a send (false: the channel is closed).
//
// go build reflect_select_over_cases_known_at_run_time_go.go && ./reflect_select_over_cases_known_at_run_time_go
package main
import (
"fmt"
"reflect"
"sort"
"testing"
)
var sink int
func main() {
// Four sensors: a number a config file could set, so no select statement can name them.
sensors := make([]chan int, 4)
cases := make([]reflect.SelectCase, len(sensors))
for i := range sensors {
sensors[i] = make(chan int, 1)
cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(sensors[i])}
}
// One channel ready at a time: the chosen index is that channel's.
for i := range sensors {
sensors[i] <- 20 + i
chosen, recv, recvOK := reflect.Select(cases)
fmt.Printf("sensor %d ready: chosen %d, value %d, recvOK %t\n", i, chosen, recv.Int(), recvOK)
}
// All ready at once: the order is random, so collect and sort.
for i := range sensors {
sensors[i] <- 30 + i
}
var got []int
for range sensors {
_, recv, _ := reflect.Select(cases)
got = append(got, int(recv.Int()))
}
sort.Ints(got)
fmt.Println("all four ready: received", got)
// Nothing ready and a default case: the default's index, and no value.
chosen, recv, recvOK := reflect.Select(withDefault(cases))
fmt.Printf("nothing ready: chosen %d (the default), recv.IsValid %t, recvOK %t\n", chosen, recv.IsValid(), recvOK)
// A closed channel is always ready: the zero value, and recvOK false.
close(sensors[2])
chosen, recv, recvOK = reflect.Select(cases)
fmt.Printf("sensor 2 closed: chosen %d, value %d, recvOK %t\n", chosen, recv.Int(), recvOK)
// Switch it off: a nil channel is never ready, and a zero Value Chan is skipped.
cases[2].Chan = reflect.ValueOf((chan int)(nil))
chosen, _, _ = reflect.Select(withDefault(cases))
fmt.Printf("sensor 2 nil: chosen %d (the default)\n", chosen)
cases[2].Chan = reflect.Value{}
chosen, _, _ = reflect.Select(withDefault(cases))
fmt.Printf("sensor 2 zero: chosen %d (the default)\n", chosen)
// A send case: Send holds the value; recv comes back invalid.
alerts := make(chan string, 1)
send := []reflect.SelectCase{{Dir: reflect.SelectSend, Chan: reflect.ValueOf(alerts), Send: reflect.ValueOf("sensor 2 offline")}}
chosen, recv, recvOK = reflect.Select(send)
fmt.Printf("send case: chosen %d, recv.IsValid %t, recvOK %t, delivered %q\n", chosen, recv.IsValid(), recvOK, <-alerts)
// What it costs, in allocations per call, against a select statement.
a, b := make(chan int, 1), make(chan int, 1)
two := []reflect.SelectCase{{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(a)}, {Dir: reflect.SelectRecv, Chan: reflect.ValueOf(b)}}
fmt.Println("allocations per call, select statement, 2 cases:", testing.AllocsPerRun(100, func() {
a <- 1
select {
case v := <-a:
sink = v
case v := <-b:
sink = v
}
}))
fmt.Println("allocations per call, reflect.Select, 2 cases:", testing.AllocsPerRun(100, func() {
a <- 1
_, v, _ := reflect.Select(two)
sink = int(v.Int())
}))
}
// withDefault returns a copy of cases with a default case appended.
func withDefault(cases []reflect.SelectCase) []reflect.SelectCase {
out := append([]reflect.SelectCase(nil), cases...)
return append(out, reflect.SelectCase{Dir: reflect.SelectDefault})
}
What to do¶
- Reach for
reflect.Selectonly when the number of cases is not known when you write the code. For two channels and a cancellation, write the statement. For "one goroutine per input channel, all forwarding into one output", Fan-out, fan-in needs no reflection and usually reads better. - Test
recvOKon every receive, and switch the case off by setting itsChantoreflect.Value{}when it is false; a closed channel left in the slice is always ready. - Keep an
opencount and stop when it reaches zero: areflect.Selectwhose every case is switched off, with no default, blocks for ever. - Read the received
Valuewith the method for its Kind —Int,String,Interface— and remember that the zero value of a closed channel's element type is a legitimateValue, not an invalid one. - Budget for it. Two allocations and a runtime call per iteration is fine for a control loop and not for a hot path.
Practice¶
Write merge(inputs []<-chan string) (orders []string, rounds int) that receives every value from any number of channels with one reflect.Select loop, drops a channel's case when it closes, ends when all have closed, and returns the values sorted plus the number of Select calls it made. Feed it three warehouse goroutines that send two, three and one order and close.
Solution
reflect_select_over_cases_known_at_run_time_kata_go.go in full — pasted here by tools/run_examples.py from the file CI runs.
// Kata: merge any number of order channels into one slice with reflect.Select,
// dropping each channel's case when it closes (recvOK false) so the loop ends
// when every channel has closed. The result is sorted, so the output does not
// depend on which warehouse was fastest.
//
// go build reflect_select_over_cases_known_at_run_time_kata_go.go && ./reflect_select_over_cases_known_at_run_time_kata_go
package main
import (
"fmt"
"reflect"
"sort"
)
func merge(inputs []<-chan string) (orders []string, rounds int) {
cases := make([]reflect.SelectCase, len(inputs))
for i, ch := range inputs {
cases[i] = reflect.SelectCase{Dir: reflect.SelectRecv, Chan: reflect.ValueOf(ch)}
}
for open := len(cases); open > 0; rounds++ {
i, v, ok := reflect.Select(cases)
if !ok {
cases[i].Chan = reflect.Value{} // a zero Chan is ignored from now on
open--
continue
}
orders = append(orders, v.String())
}
sort.Strings(orders)
return orders, rounds
}
func warehouse(name string, count int) <-chan string {
out := make(chan string)
go func() {
defer close(out)
for i := 1; i <= count; i++ {
out <- fmt.Sprintf("%s-order-%d", name, i)
}
}()
return out
}
func main() {
inputs := []<-chan string{warehouse("east", 2), warehouse("west", 3), warehouse("north", 1)}
orders, rounds := merge(inputs)
fmt.Println(len(orders), "orders from", len(inputs), "channels in", rounds, "rounds of reflect.Select")
for _, o := range orders {
fmt.Println(" ", o)
}
}
Verified output of reflect_select_over_cases_known_at_run_time_kata_go.go — regenerated by tools/run_examples.py, never hand-typed.
In other languages¶
- Concurrency library — How does one thread wait on several channels at once? ↗ is a stub that asks this question of six languages; Go's answer is the statement, and this page is the run-time form of it.
- Rust — the Rust library's
crossbeamchannels ↗ covers theselect!macro, whose arms are fixed in the source like Go's statement; the crate'sSelect↗ type is the run-time form, built by addingrecvandsendoperations in a loop and returning the index of the ready one, asreflect.Selectdoes — without reflection, because the operations are typed closures. - Python has no channels;
asyncio.wait↗ over a list of tasks withFIRST_COMPLETEDis the nearest thing to selecting over a number of sources known only at run time.
Sources¶
reflect.Select↗ andreflect.SelectCase↗ — the semantics, the zero-Chanrule, and the 65536-case maximum.- The Go specification, Select statements ↗ — the statement this function reproduces.
src/reflect/value.goat go1.25.5 ↗ —Select, and the panics for aSendon a receive case or a receive-only channel in a send case.- Adam Freeman, Pro Go (Apress, 2022), chapter 29, "Using Reflection, Part 3" — "Working with Channel Values" and "Selecting from Multiple Channels".
- Burak Serdar, Effective Concurrency in Go (Packt, 2023), chapter 5, "Worker Pools and Pipelines" — the side note on
reflect.Selectfor a fan-in over a dynamic number of channels.