Go Select Statement

Choose between multiple channel operations

🔀 What is Select Statement?

Select statement lets goroutines wait on multiple channel operations simultaneously. It's like a switch statement but for channels, choosing the first available channel operation to execute.


// Basic select example
package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "Channel 1"
    }()
    
    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "Channel 2"
    }()
    
    select {
    case msg1 := <-ch1:
        fmt.Println("Received:", msg1)
    case msg2 := <-ch2:
        fmt.Println("Received:", msg2)
    }
}
                                    

Select Statement Features

Non-blocking

Use default case for non-blocking operations

select {
case msg := <-ch:
    fmt.Println(msg)
default:
    fmt.Println("No data")
}
🎲

Random Choice

Randomly selects if multiple cases ready

select {
case <-ch1:
    // Handle ch1
case <-ch2:
    // Handle ch2
}

Timeout

Implement timeouts with time.After

select {
case msg := <-ch:
    // Handle message
case <-time.After(5*time.Second):
    // Handle timeout
}
🔄

Multiplexing

Handle multiple channels in one place

for {
    select {
    case <-done:
        return
    case work := <-jobs:
        process(work)
    }
}

🔹 Basic Select Usage

Select waits for one of its cases to be ready:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)
    
    // Send to ch1 after 1 second
    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "Fast channel"
    }()
    
    // Send to ch2 after 3 seconds
    go func() {
        time.Sleep(3 * time.Second)
        ch2 <- "Slow channel"
    }()
    
    // Select will choose the first ready channel
    select {
    case msg := <-ch1:
        fmt.Println("Got from ch1:", msg)
    case msg := <-ch2:
        fmt.Println("Got from ch2:", msg)
    }
}

Output:

Got from ch1: Fast channel

🔹 Default Case (Non-blocking)

Use default case to make select non-blocking:

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 data available")
    }
    
    // Send data in goroutine
    go func() {
        time.Sleep(1 * time.Second)
        ch <- "Hello!"
    }()
    
    // Poll for data
    for i := 0; i < 5; i++ {
        select {
        case msg := <-ch:
            fmt.Println("Finally got:", msg)
            return
        default:
            fmt.Printf("Waiting... (%d)\n", i+1)
            time.Sleep(300 * time.Millisecond)
        }
    }
}

Output:

No data available
Waiting... (1)
Waiting... (2)
Waiting... (3)
Waiting... (4)
Finally got: Hello!

🔹 Timeout Pattern

Implement timeouts using time.After:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)
    
    // Simulate slow operation
    go func() {
        time.Sleep(3 * time.Second)
        ch <- "Operation completed"
    }()
    
    // Wait with timeout
    select {
    case result := <-ch:
        fmt.Println("Success:", result)
    case <-time.After(2 * time.Second):
        fmt.Println("Timeout: Operation took too long")
    }
}

Output:

Timeout: Operation took too long

🔹 Channel Multiplexing

Handle multiple channels in a loop with select:

package main

import (
    "fmt"
    "time"
)

func main() {
    jobs := make(chan string, 5)
    done := make(chan bool)
    
    // Send some jobs
    jobs <- "Job 1"
    jobs <- "Job 2"
    jobs <- "Job 3"
    close(jobs)
    
    // Process jobs with timeout
    go func() {
        for {
            select {
            case job, ok := <-jobs:
                if !ok {
                    fmt.Println("All jobs completed")
                    done <- true
                    return
                }
                fmt.Printf("Processing: %s\n", job)
                time.Sleep(500 * time.Millisecond)
            case <-time.After(2 * time.Second):
                fmt.Println("Worker timeout")
                done <- true
                return
            }
        }
    }()
    
    <-done
    fmt.Println("Program finished")
}

Output:

Processing: Job 1
Processing: Job 2
Processing: Job 3
All jobs completed
Program finished

🔹 Select Best Practices

Follow these patterns for effective select usage:

✅ Common Patterns:

  • Timeout: Use time.After for operation timeouts
  • Non-blocking: Use default case for polling
  • Cancellation: Use context.Done() for cancellation
  • Fan-in: Merge multiple channels into one

⚠️ Watch Out For:

  • Empty select: select{} blocks forever
  • Nil channels: Never selected in select
  • Closed channels: Always ready for receive
// Fan-in pattern: merge multiple channels
func fanIn(ch1, ch2 <-chan string) <-chan string {
    out := make(chan string)
    go func() {
        defer close(out)
        for {
            select {
            case msg, ok := <-ch1:
                if !ok {
                    ch1 = nil // Disable this case
                    continue
                }
                out <- msg
            case msg, ok := <-ch2:
                if !ok {
                    ch2 = nil // Disable this case
                    continue
                }
                out <- msg
            }
            if ch1 == nil && ch2 == nil {
                return
            }
        }
    }()
    return out
}

🧠 Test Your Knowledge

What happens when multiple cases in a select are ready?