Go Select — Multiplexing Channel Operations with Select Statement
In this tutorial, you will learn about Go Select. We cover key concepts, practical examples, and best practices to help you master this topic.
Go select statement multiplexes channel operations with case branches for non-blocking sends, receives, timeouts, and default cases.
What You'll Learn
- Select syntax and semantics
- Non-blocking channel operations
- Timeouts with time.After
- Random selection behavior
Why It Matters
Select enables responsive concurrent programs. Docker uses select for stream multiplexing. Kubernetes uses select for event loops. DodaZIP uses select for graceful shutdown.
Real-World Use
Server event loops, request multiplexing, health check monitoring, graceful shutdown handling, load balancing.
flowchart LR
A["Select"] --> B["Basic Select"]
A --> C["Non-Blocking"]
A --> D["Timeout"]
A --> E["Default"]
A:::current --> B
style A fill:#2563eb,stroke:#2563eb,color:#fff
style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b
Basic Select
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(100 * time.Millisecond)
ch1 <- "one"
}()
go func() {
time.Sleep(200 * time.Millisecond)
ch2 <- "two"
}()
select {
case msg := <-ch1:
fmt.Println("Received from ch1:", msg)
case msg := <-ch2:
fmt.Println("Received from ch2:", msg)
}
}
Non-Blocking Send/Receive
func main() {
ch := make(chan int, 1)
ch <- 42
select {
case val := <-ch:
fmt.Println("Received:", val)
default:
fmt.Println("No value available")
}
select {
case ch <- 100:
fmt.Println("Sent 100")
default:
fmt.Println("Channel full")
}
}
Timeout with Select
func main() {
ch := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch <- "result"
}()
select {
case res := <-ch:
fmt.Println(res)
case <-time.After(1 * time.Second):
fmt.Println("timeout")
}
}
Continuous Select Loop
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
go func() {
for i := 0; i < 5; i++ {
ch1 <- i
time.Sleep(100 * time.Millisecond)
}
close(ch1)
}()
go func() {
for i := 10; i < 15; i++ {
ch2 <- i
time.Sleep(150 * time.Millisecond)
}
close(ch2)
}()
for ch1 != nil || ch2 != nil {
select {
case val, ok := <-ch1:
if !ok { ch1 = nil; continue }
fmt.Println("ch1:", val)
case val, ok := <-ch2:
if !ok { ch2 = nil; continue }
fmt.Println("ch2:", val)
}
}
}
Fan-Out with Select
func main() {
jobs := make(chan int, 10)
done := make(chan bool)
for w := 1; w <= 3; w++ {
go func(id int) {
for job := range jobs {
fmt.Printf("Worker %d: %d\n", id, job)
time.Sleep(time.Second)
}
done <- true
}(w)
}
for j := 1; j <= 5; j++ {
jobs <- j
}
close(jobs)
for w := 1; w <= 3; w++ {
<-done
}
}
Graceful Shutdown
func main() {
jobs := make(chan int, 5)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go func() {
for i := 0; i < 10; i++ {
jobs <- i
time.Sleep(500 * time.Millisecond)
}
close(jobs)
}()
for {
select {
case job, ok := <-jobs:
if !ok { return }
fmt.Println("Processing:", job)
case sig := <-quit:
fmt.Println("Shutting down, signal:", sig)
return
}
}
}
Common Mistakes
1. Select with nil Channels
var ch chan int
select {
case <-ch: // Never selected — nil channel blocks
default:
}
2. Deadlock with Select
select {} // Blocks forever! No cases, no default
3. Not Checking Channel Close
select {
case val := <-ch: // Returns zero value if closed!
// Check with: val, ok := <-ch
}
4. Missing Default for Non-Blocking
select {
case <-ch:
// May block if no value
// Add default: for non-blocking behavior
}
5. Long-Running Cases
Avoid heavy computation in select cases. Process channel values quickly and use goroutines for heavy work.
Practice Questions
1. What happens if multiple cases are ready? Go selects one randomly. All ready cases have equal chance.
2. How do you implement a timeout? Use time.After(duration) as a case. Returns a channel that fires after the duration.
3. What does select with nil channels do? nil channels are never selected. Useful for disabling cases dynamically by setting channels to nil.
4. Can select have a default case? Yes. It executes immediately if no other case is ready, making the select non-blocking.
Challenge: Build a rate-limited request handler using select with a ticker.
Solution
func main() {
requests := make(chan int, 10)
for i := 0; i < 10; i++ { requests <- i }
close(requests)
ticker := time.NewTicker(200 * time.Millisecond)
defer ticker.Stop()
for req := range requests {
<-ticker.C
fmt.Println("Processing request", req)
}
}
FAQ
{{< faq question="Is select deterministic?" >}} No. When multiple cases are ready, Go picks one randomly. This prevents starvation and forces you to handle all cases correctly. {{< /faq >}}
{{< faq question="Can select be used outside of goroutines?" >}} Yes. Select works in any Goroutine, including the main goroutine. It's commonly used in main() for server loops. {{< /faq >}}
{{< faq question="What is a nil channel in select?" >}} Reading from or writing to a nil channel blocks forever. Setting a channel to nil in select effectively disables that case. {{< /faq >}}
{{< faq question="How do I break from a select loop?" >}}
Use return to exit the containing function, or break with a label to break out of the loop containing the select.
{{< /faq >}}
{{< faq question="Can select have send operations?" >}}
Yes. Select supports both send (ch <- val) and receive (<-ch) cases. All must be channel operations.
{{< /faq >}}
Try It Yourself
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan string, 2)
ch <- "fast"
select {
case msg := <-ch:
fmt.Println("Got:", msg)
case <-time.After(100 * time.Millisecond):
fmt.Println("Timeout")
default:
fmt.Println("Nothing available")
}
}
Expected output:
Got: fast
What's Next
Now that you understand select, learn about WaitGroups for goroutine synchronization.
| Topic | Description | Link |
|---|---|---|
| Go WaitGroups | Goroutine synchronization | {{< ref "20-waitgroups" >}} |
| Go Mutexes | Shared state protection | {{< ref "21-mutexes" >}} |
| Rust Channels | Compare Rust's concurrency | Rust |