The select statement is Go's way to wait on multiple channel operations simultaneously. It's similar to a switch statement but for channels, allowing you to handle multiple communication paths elegantly.
Basic syntax
select {
case <-ch1:
// Handle message from ch1
case msg := <-ch2:
// Handle message from ch2 and use the value
case ch3 <- value:
// Successfully sent value to ch3
default:
// Execute if no channel is ready (optional)
}
Without select, you can only wait on one channel at a time:
// ❌ Can only wait on one channel
msg1 := <-ch1 // Blocks here, can't check ch2
msg2 := <-ch2 // Never reached if ch1 blocks
With select, you can wait on multiple channels:
// ✅ Can wait on multiple channels
select {
case msg1 := <-ch1:
// Handle ch1
case msg2 := <-ch2:
// Handle ch2
}
The select statement evaluates all cases to check which channels are ready. If multiple channels have data, one is chosen randomly to prevent starvation. If no channel is ready and there's no default, it blocks until one becomes ready. Only one case executes per select statement.
Basic example
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(1 * time.Second)
ch1 <- "Message from ch1"
}()
go func() {
time.Sleep(2 * time.Second)
ch2 <- "Message from ch2"
}()
// Wait for whichever channel is ready first
select {
case msg1 := <-ch1:
fmt.Println("Received from ch1:", msg1)
case msg2 := <-ch2:
fmt.Println("Received from ch2:", msg2)
}
fmt.Println("Select completed")
}
Output (ch1 will likely be ready first)
Received from ch1: Message from ch1
Select completed
Select picks randomly when multiple channels are ready to prevent channel starvation. If one channel always had priority, other channels might never be serviced. Random selection ensures fairness.
Example demonstrating random selection
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// Fill both channels
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
}
}()
go func() {
for i := 10; i < 15; i++ {
ch2 <- i
}
}()
time.Sleep(100 * time.Millisecond) // Let channels fill
// Select will randomly pick between ready channels
for i := 0; i < 10; i++ {
select {
case v1 := <-ch1:
fmt.Printf("Got from ch1: %d\n", v1)
case v2 := <-ch2:
fmt.Printf("Got from ch2: %d\n", v2)
}
}
}
The default case makes select non-blocking.
Non-blocking operations are useful when you want to check if a channel is ready without blocking:
select {
case msg := <-ch:
// Handle message
default:
// Channel not ready, continue without blocking
fmt.Println("No message available")
}
The default case works by executing immediately if no channel in the select is ready, making the select non-blocking.
Example
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string)
// Try to receive without blocking
select {
case msg := <-ch:
fmt.Println("Received:", msg)
default:
fmt.Println("No message, continuing...")
}
// Send a message in background
go func() {
time.Sleep(100 * time.Millisecond)
ch <- "Hello"
}()
// Try again after a delay
time.Sleep(200 * time.Millisecond)
select {
case msg := <-ch:
fmt.Println("Received:", msg)
default:
fmt.Println("No message")
}
}
Non-blocking send
You can also do non-blocking sends:
select {
case ch <- value:
fmt.Println("Sent successfully")
default:
fmt.Println("Channel full, couldn't send")
}
Non-blocking operations are useful because they allow you to check channel state to see if a channel is ready without waiting, implement timeouts by combining with timers for timeout logic, and avoid deadlocks by continuing execution even if channels aren't ready.
The default case executes when no other case is ready.
When to use default
Example: Non-blocking channel check
package main
import (
"fmt"
"time"
)
func worker(ch <-chan int, done chan<- bool) {
for {
select {
case val := <-ch:
fmt.Printf("Worker: Processing %d\n", val)
default:
// No work available, do other things
fmt.Println("Worker: No work, checking again...")
time.Sleep(100 * time.Millisecond)
}
// Check if we should exit
select {
case <-done:
fmt.Println("Worker: Exiting")
return
default:
// Continue working
}
}
}
func main() {
ch := make(chan int)
done := make(chan bool)
go worker(ch, done)
// Send some work
time.Sleep(200 * time.Millisecond)
ch <- 1
ch <- 2
time.Sleep(500 * time.Millisecond)
done <- true
time.Sleep(100 * time.Millisecond)
}
Important: default in loops
When using default in a loop, be careful not to create a busy-wait:
// ⚠️ BAD: Busy-wait (consumes CPU)
for {
select {
case msg := <-ch:
process(msg)
default:
// Loops continuously, wasting CPU
}
}
// ✅ GOOD: Add a small sleep or use blocking select
for {
select {
case msg := <-ch:
process(msg)
case <-time.After(100 * time.Millisecond):
// Periodic check, not busy-wait
}
}
Timeouts are essential for preventing goroutines from waiting indefinitely.
Timeouts matter because without them, a goroutine might wait forever if a channel never receives data, causing deadlocks where the program hangs waiting, resource leaks where goroutines never terminate, and poor user experience where applications become unresponsive.
To implement timeouts, use time.After to create a timeout channel:
select {
case msg := <-ch:
// Handle message
case <-time.After(5 * time.Second):
// Timeout after 5 seconds
fmt.Println("Operation timed out")
}
Example
package main
import (
"fmt"
"time"
)
func fetchData(ch chan<- string) {
// Simulate slow operation
time.Sleep(3 * time.Second)
ch <- "Data received"
}
func main() {
ch := make(chan string)
go fetchData(ch)
select {
case data := <-ch:
fmt.Println("Success:", data)
case <-time.After(2 * time.Second):
fmt.Println("Timeout: Operation took too long")
}
}
Output
Timeout: Operation took too long
time.After(duration) works by returning a channel that receives a value after the specified duration. This channel can be used in select to implement timeouts.
Reusable timeout pattern
package main
import (
"fmt"
"time"
)
func operationWithTimeout(ch <-chan string, timeout time.Duration) (string, error) {
select {
case result := <-ch:
return result, nil
case <-time.After(timeout):
return "", fmt.Errorf("operation timed out after %v", timeout)
}
}
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "Result"
}()
result, err := operationWithTimeout(ch, 1*time.Second)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
Context-based timeouts
For more sophisticated timeout handling, use context (covered in section 6.5: Patterns and Best Practices):
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
select {
case result := <-ch:
return result
case <-ctx.Done():
return "", ctx.Err()
}
The select statement enables several powerful concurrency patterns.
1. Fan-in pattern (multiple inputs, one output)
package main
import (
"fmt"
"sync"
)
func merge(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
wg.Add(2)
// Forward from ch1
go func() {
defer wg.Done()
for val := range ch1 {
out <- val
}
}()
// Forward from ch2
go func() {
defer wg.Done()
for val := range ch2 {
out <- val
}
}()
// Close output when both inputs are done
go func() {
wg.Wait()
close(out)
}()
return out
}
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// Send to both channels
go func() {
defer close(ch1)
for i := 0; i < 5; i++ {
ch1 <- i
}
}()
go func() {
defer close(ch2)
for i := 10; i < 15; i++ {
ch2 <- i
}
}()
// Merge and receive
for val := range merge(ch1, ch2) {
fmt.Println("Received:", val)
}
}
2. Priority channel pattern
package main
import (
"fmt"
"time"
)
func main() {
highPriority := make(chan string)
lowPriority := make(chan string)
go func() {
for i := 0; i < 3; i++ {
lowPriority <- fmt.Sprintf("Low %d", i)
time.Sleep(200 * time.Millisecond)
}
}()
go func() {
time.Sleep(500 * time.Millisecond)
highPriority <- "High priority message"
}()
// Check high priority first, then low priority
for i := 0; i < 4; i++ {
select {
case msg := <-highPriority:
fmt.Println("HIGH:", msg)
case msg := <-lowPriority:
fmt.Println("LOW:", msg)
}
}
}
3. Graceful shutdown pattern
package main
import (
"fmt"
"time"
)
func worker(work <-chan int, shutdown <-chan bool) {
for {
select {
case task := <-work:
fmt.Printf("Processing task %d\n", task)
case <-shutdown:
fmt.Println("Shutdown signal received")
return
}
}
}
func main() {
work := make(chan int)
shutdown := make(chan bool)
go worker(work, shutdown)
// Send some work
for i := 0; i < 3; i++ {
work <- i
time.Sleep(100 * time.Millisecond)
}
// Shutdown
shutdown <- true
time.Sleep(100 * time.Millisecond)
}
4. Ticker pattern (periodic operations)
package main
import (
"fmt"
"time"
)
func main() {
ticker := time.NewTicker(500 * time.Millisecond)
defer ticker.Stop()
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "Important message"
}()
for {
select {
case msg := <-ch:
fmt.Println("Received:", msg)
return
case <-ticker.C:
fmt.Println("Tick: Still waiting...")
}
}
}
5. Multiple operations with timeout
package main
import (
"fmt"
"time"
)
func operation1(ch chan<- string) {
time.Sleep(1 * time.Second)
ch <- "Operation 1 complete"
}
func operation2(ch chan<- string) {
time.Sleep(2 * time.Second)
ch <- "Operation 2 complete"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go operation1(ch1)
go operation2(ch2)
timeout := time.After(3 * time.Second)
for i := 0; i < 2; i++ {
select {
case result1 := <-ch1:
fmt.Println(result1)
case result2 := <-ch2:
fmt.Println(result2)
case <-timeout:
fmt.Println("Overall timeout")
return
}
}
}
The select statement is one of Go's most powerful concurrency primitives. It enables elegant handling of multiple channels, timeouts, and non-blocking operations, making it essential for writing robust concurrent programs.