Skip to main content
The select statement blocks the code and waits for multiple channel operations simultaneously. A select blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple are ready.

Basic Select Example

Let’s look at a simple example:
package main

import (
	"fmt"
	"time"
)

func main() {
	one := make(chan string)
	two := make(chan string)

	go func() {
		time.Sleep(time.Second * 2)
		one <- "One"
	}()

	go func() {
		time.Sleep(time.Second * 1)
		two <- "Two"
	}()

	select {
	case result := <-one:
		fmt.Println("Received:", result)
	case result := <-two:
		fmt.Println("Received:", result)
	}

	close(one)
	close(two)
}
In this example, the select statement waits for either channel one or two to receive a value. Since channel two receives a value first (after 1 second), that case will execute.
$ go run main.go
Received: Two
The select statement is similar to switch, but it works with channels instead of values.

Default Case for Non-blocking Operations

Similar to switch, select also has a default case that runs if no other case is ready. This helps us send or receive without blocking:
func main() {
	one := make(chan string)
	two := make(chan string)

	for x := 0; x < 10; x++ {
		go func() {
			time.Sleep(time.Second * 2)
			one <- "One"
		}()

		go func() {
			time.Sleep(time.Second * 1)
			two <- "Two"
		}()
	}

	for x := 0; x < 10; x++ {
		select {
		case result := <-one:
			fmt.Println("Received:", result)
		case result := <-two:
			fmt.Println("Received:", result)
		default:
			fmt.Println("Default...")
			time.Sleep(200 * time.Millisecond)
		}
	}

	close(one)
	close(two)
}
In this example, the default case executes when neither channel is ready, preventing the program from blocking.
$ go run main.go
Default...
Default...
Default...
Default...
Default...
Received: Two
Received: One
Received: Two
Received: Two
Received: One
The default case makes select non-blocking. Without it, select will wait until at least one channel is ready.

Timeout Patterns

One common use case for select is implementing timeouts. We can use time.After which returns a channel that sends a value after a specified duration:
package main

import (
	"fmt"
	"time"
)

func main() {
	ch := make(chan string)

	go func() {
		time.Sleep(3 * time.Second)
		ch <- "result"
	}()

	select {
	case result := <-ch:
		fmt.Println("Received:", result)
	case <-time.After(2 * time.Second):
		fmt.Println("Timeout: operation took too long")
	}

	close(ch)
}
$ go run main.go
Timeout: operation took too long
This pattern is extremely useful for implementing timeouts in network operations, database queries, or any long-running operations.

Empty Select

It’s important to know that an empty select {} blocks forever:
func main() {
	ch := make(chan string)

	go func() {
		time.Sleep(1 * time.Second)
		ch <- "Hello"
	}()

	select {} // This blocks forever

	close(ch) // This line never executes
}
An empty select statement will block the goroutine forever. This is sometimes used intentionally to keep a program running, but be careful not to use it accidentally.

Select with Multiple Channels

You can have multiple cases in a select statement, making it powerful for coordinating between many goroutines:
package main

import (
	"fmt"
	"time"
)

func main() {
	ch1 := make(chan string)
	ch2 := make(chan string)
	ch3 := make(chan string)

	go func() {
		time.Sleep(1 * time.Second)
		ch1 <- "Channel 1"
	}()

	go func() {
		time.Sleep(2 * time.Second)
		ch2 <- "Channel 2"
	}()

	go func() {
		time.Sleep(3 * time.Second)
		ch3 <- "Channel 3"
	}()

	for i := 0; i < 3; i++ {
		select {
		case msg1 := <-ch1:
			fmt.Println("Received:", msg1)
		case msg2 := <-ch2:
			fmt.Println("Received:", msg2)
		case msg3 := <-ch3:
			fmt.Println("Received:", msg3)
		}
	}
}
The select statement is one of the most powerful features in Go for building concurrent applications. It allows you to elegantly handle multiple channel operations in a clean and readable way.

Build docs developers (and LLMs) love