select Statement — Waiting on Multiple Channels
The select statement lets you wait on multiple channel operations simultaneously. It looks similar to switch, but each case is a channel operation.
select Basic Syntax
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"
}()
// Handle whichever channel is ready first
for i := 0; i < 2; i++ {
select {
case msg := <-ch1:
fmt.Println("ch1:", msg)
case msg := <-ch2:
fmt.Println("ch2:", msg)
}
}
}
// Output:
// ch1: message from ch1 (after 1 second)
// ch2: message from ch2 (1 more second later)
select behavior rules:
- Multiple cases ready simultaneously— one is chosen at random
- No cases ready— blocks and waits
defaultcase present— executes immediately without blocking
Timeout Pattern
Use time.After to set a time limit on network requests or external operations.
package main
import (
"fmt"
"time"
)
func slowOperation() <-chan string {
ch := make(chan string)
go func() {
time.Sleep(3 * time.Second) // Simulate slow work
ch <- "done"
}()
return ch
}
func main() {
result := slowOperation()
select {
case res := <-result:
fmt.Println("Result:", res)
case <-time.After(2 * time.Second): // 2-second timeout
fmt.Println("Timeout! Operation took too long.")
}
}
time.After(d) returns a channel that sends a value after the specified duration. Very convenient for implementing timeouts.
default — Non-blocking Receive/Send
Adding a default case makes select return immediately without blocking.
package main
import "fmt"
func main() {
ch := make(chan int, 1)
// Non-blocking send
select {
case ch <- 42:
fmt.Println("Send succeeded")
default:
fmt.Println("Channel full, send failed")
}
// Non-blocking receive
select {
case v := <-ch:
fmt.Println("Received:", v)
default:
fmt.Println("Channel is empty")
}
// Second receive attempt (already empty)
select {
case v := <-ch:
fmt.Println("Received:", v)
default:
fmt.Println("Channel is empty")
}
}
// Output:
// Send succeeded
// Received: 42
// Channel is empty
done Channel Pattern — Goroutine Termination Signal
Send a termination signal to goroutines via a done channel. One of the most commonly used patterns in Go.
package main
import (
"fmt"
"time"
)
func worker(done <-chan struct{}) {
for {
select {
case <-done:
fmt.Println("Worker exiting")
return
default:
// Perform actual work
fmt.Println("Working...")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
done := make(chan struct{})
go worker(done)
time.Sleep(2 * time.Second)
close(done) // Send termination signal (close, not send)
time.Sleep(100 * time.Millisecond)
fmt.Println("Main exiting")
}
Using close(done) sends a signal to all goroutines waiting on that channel simultaneously.
Priority Processing Pattern
Use this when you need to separate high-priority tasks from regular ones.
package main
import (
"fmt"
)
func prioritySelect(high, low <-chan string) {
for {
// Check high-priority channel first
select {
case msg := <-high:
fmt.Println("[HIGH]", msg)
continue
default:
}
// If no high-priority, wait on both
select {
case msg := <-high:
fmt.Println("[HIGH]", msg)
case msg := <-low:
fmt.Println("[LOW]", msg)
}
}
}
func main() {
high := make(chan string, 5)
low := make(chan string, 5)
high <- "Urgent alert 1"
high <- "Urgent alert 2"
low <- "Regular task 1"
low <- "Regular task 2"
high <- "Urgent alert 3"
go prioritySelect(high, low)
var input string
fmt.Scanln(&input)
}
Real-World Example — Retry Logic with Timeout
package main
import (
"errors"
"fmt"
"math/rand"
"time"
)
// Simulate an occasionally failing external service
func callExternalService() <-chan string {
ch := make(chan string, 1)
go func() {
delay := time.Duration(rand.Intn(3)+1) * time.Second
time.Sleep(delay)
if rand.Intn(2) == 0 {
ch <- "success response"
}
// On failure, send nothing
}()
return ch
}
func callWithRetry(maxRetries int, timeout time.Duration) (string, error) {
for i := 0; i < maxRetries; i++ {
fmt.Printf("Attempt %d/%d...\n", i+1, maxRetries)
result := callExternalService()
select {
case res := <-result:
return res, nil
case <-time.After(timeout):
fmt.Println("Timeout, retrying")
continue
}
}
return "", errors.New("max retries exceeded")
}
func main() {
rand.Seed(time.Now().UnixNano())
result, err := callWithRetry(3, 1500*time.Millisecond)
if err != nil {
fmt.Println("Error:", err)
} else {
fmt.Println("Result:", result)
}
}
Using nil Channels in select
Operations on a nil channel block forever. This can be used to dynamically disable a case in select.
package main
import "fmt"
func merge(ch1, ch2 <-chan int) <-chan int {
out := make(chan int)
go func() {
defer close(out)
for ch1 != nil || ch2 != nil {
select {
case v, ok := <-ch1:
if !ok {
ch1 = nil // Set to nil when closed → disables this case
continue
}
out <- v
case v, ok := <-ch2:
if !ok {
ch2 = nil
continue
}
out <- v
}
}
}()
return out
}
func main() {
ch1 := make(chan int, 3)
ch2 := make(chan int, 3)
ch1 <- 1
ch1 <- 3
ch1 <- 5
close(ch1)
ch2 <- 2
ch2 <- 4
ch2 <- 6
close(ch2)
for v := range merge(ch1, ch2) {
fmt.Printf("%d ", v)
}
fmt.Println()
}
Key Takeaways
select— handles whichever channel operation is ready (random selection on tie)time.After— convenient timeout implementationdefault— non-blocking channel operationclose(done)— send simultaneous termination signal to multiple goroutines- nil channel— permanently blocks that case → use for dynamic deactivation