Skip to content
AgileLogics
6 min read

select

Prerequisites: Channels

select waits on several channel operations at once and runs the first one that is ready. It looks like a switch, but each case is a send or a receive.

  • If several cases are ready at the same time, one is chosen at random.
  • A default case runs immediately when no other case is ready, making the select non-blocking.
  • time.After(d) returns a channel that receives a value after d, which makes timeouts a one-liner.

Syntax

select {
case msg := <-messages:
    fmt.Println("received", msg)
case results <- value:
    fmt.Println("sent", value)
case <-time.After(time.Second):
    fmt.Println("timed out")
default:
    fmt.Println("nothing ready")
}

Example

package main

import (
    "fmt"
    "time"
)

// fetch simulates a slow operation that delivers its result on a channel.
func fetch(delay time.Duration, result string) <-chan string {
    ch := make(chan string, 1) // buffered so the goroutine never blocks forever
    go func() {
        time.Sleep(delay)
        ch <- result
    }()
    return ch
}

func main() {
    // Take whichever result arrives first.
    fast := fetch(10*time.Millisecond, "fast")
    slow := fetch(300*time.Millisecond, "slow")
    select {
    case r := <-fast:
        fmt.Println("got", r)
    case r := <-slow:
        fmt.Println("got", r)
    }

    // Give up after a timeout.
    select {
    case r := <-fetch(500*time.Millisecond, "too late"):
        fmt.Println("got", r)
    case <-time.After(50 * time.Millisecond):
        fmt.Println("timed out")
    }

    // Non-blocking receive with default.
    msgs := make(chan string, 1)
    select {
    case m := <-msgs:
        fmt.Println("received", m)
    default:
        fmt.Println("no message ready")
    }

    msgs <- "hi"
    select {
    case m := <-msgs:
        fmt.Println("received", m)
    default:
        fmt.Println("no message ready")
    }
}

Expected output

got fast
timed out
no message ready
received hi

The first two results depend on timing, but the delays are far enough apart that you will see this output in practice.

A common pattern is a for-select loop that processes work until told to stop:

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

Common mistakes

  • Using default inside a for loop with nothing else to wait on — the loop spins and burns 100% of a CPU core. Only add default when you really want a non-blocking check.
  • Creating a new time.After on every iteration of a busy loop when you meant one overall deadline. Create the timer channel once, outside the loop, or use time.NewTimer / time.NewTicker.
  • Expecting cases to be tried top to bottom. When several are ready, the choice is random.
  • Returning an unbuffered channel from a function like fetch. If the caller times out and never receives, the goroutine blocks on its send forever — a goroutine leak.

Practice

Write firstOrTimeout(ch <-chan int, d time.Duration) (int, bool) that returns the first value from ch, or 0, false if nothing arrives within d. Test it with a buffered channel that already holds 42, and with an empty channel.

Show solution
package main

import (
    "fmt"
    "time"
)

func firstOrTimeout(ch <-chan int, d time.Duration) (int, bool) {
    select {
    case v := <-ch:
        return v, true
    case <-time.After(d):
        return 0, false
    }
}

func main() {
    ready := make(chan int, 1)
    ready <- 42
    fmt.Println(firstOrTimeout(ready, 100*time.Millisecond)) // 42 true

    empty := make(chan int)
    fmt.Println(firstOrTimeout(empty, 50*time.Millisecond)) // 0 false
}