select で複数の channel を待ち受ける

複数の channel からデータを受信したいとき、どの channel から先にデータが届くかわからないことがあります。そんなときに使うのが select です。

select の基本

select は switch に似た構文ですが、channel の操作を待ち受けます。

select {
case v := <-ch1:
    fmt.Println("ch1 から受信:", v)
case v := <-ch2:
    fmt.Println("ch2 から受信:", v)
}

複数の case のうち、最初に通信可能になったものが実行されます。どちらも準備できていない場合は、どちらかが準備できるまでブロックします。

実際に使ってみる

2 つの goroutine が異なるタイミングでデータを送信する例です。

package main

import (
    "fmt"
    "time"
)

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"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg := <-ch1:
            fmt.Println(msg)
        case msg := <-ch2:
            fmt.Println(msg)
        }
    }
}

ch1 が 100ms 後、ch2 が 200ms 後に送信します。select は準備ができた順に受信するので、「one」「two」の順で出力されます。

default でブロックを回避する

default を入れると、どの channel も準備できていないときにすぐ実行されます。

select {
case v := <-ch:
    fmt.Println("受信:", v)
default:
    fmt.Println("データなし")
}

これはノンブロッキングな受信に使えます。データがあれば受け取り、なければ他の処理を進める、という動きになります。

タイムアウトを実装する

time.After と組み合わせると、タイムアウト処理を簡潔に書けます。

package main

import (
    "fmt"
    "time"
)

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

    go func() {
        time.Sleep(2 * time.Second)
        ch <- "結果"
    }()

    select {
    case result := <-ch:
        fmt.Println(result)
    case <-time.After(1 * time.Second):
        fmt.Println("タイムアウト")
    }
}

time.After は指定時間後に値を送信する channel を返します。本来の処理が 1 秒以内に終わらなければ、タイムアウトの case が実行されます。

select の注意点

複数の case が同時に準備できた場合、どれが選ばれるかはランダムです。特定の順序を期待してはいけません。

また、空の select(case がない)は永遠にブロックします。

select {} // 永遠にブロック

これはプログラムを終了させたくないときに使うことがありますが、通常のコードでは避けるべきです。