Go言語のselect文の使い方を完全ガイド!初心者でもわかる並行処理の条件分岐
生徒
「Go言語で複数の処理を同時に動かしたいときって、どうやって条件分けするんですか?」
先生
「Go言語では、select文を使えば、複数の処理を並行(同時)に実行しながら、条件分岐をすることができますよ。」
生徒
「それって難しそう…並行処理とかよくわかりません。」
先生
「大丈夫です!このあと、初心者にもわかりやすく丁寧に解説していきます。一緒に学んでいきましょう!」
1. Go言語のselect文とは?
select文は、Go言語で並行処理(goroutine)を扱うときに使う、チャネル受信に特化した条件分岐です。複数の処理が同時進行しているとき、「どのチャネルから先に届いたか」に応じて分岐します。先着順で処理を振り分けるスイッチのような役割、と覚えると理解しやすいです。
Goでは、軽量スレッドであるgoroutineが仕事を並行して進め、結果や通知はchannel(チャネル)で受け渡しします。selectはその受け取り口をひとまとめにして、最初に準備ができたチャネルの処理だけを実行します。待ち合わせの行列で、先に呼ばれた窓口に進むイメージです。
まずは雰囲気をつかむための最小サンプルです。細かな仕組みは後の章で解説しますが、「どちらか先に届いた方を表示する」動きを確認できます。
package main
import "fmt"
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() { ch1 <- "一番乗りはch1" }()
go func() { ch2 <- "一番乗りはch2" }()
select {
case msg := <-ch1:
fmt.Println(msg) // ch1が先ならこちら
case msg := <-ch2:
fmt.Println(msg) // ch2が先ならこちら
}
}
このようにselectは、複数のチャネルを同時に監視し、準備できた1つだけを選んで処理します。並行処理・非同期処理で起こりがちな「どれが先に終わるか分からない」状況でも、シンプルな書き方で安全に分岐できるのが特徴です。
2. select文の基本構文
select文は、チャネルを使った送受信を条件として分岐します。各caseには「受信(x := <-ch)」または「送信(ch <- v)」のいずれかを書き、準備ができたものだけが実行されます。複数が同時に準備できていれば、その中から1つが選ばれます。どれも準備できていない場合、defaultが無ければ待機、defaultがあればすぐにそちらが走ります。
select {
case v := <-ch1:
// ch1から受信できたときの処理(vを使う)
case ch2 <- 10:
// ch2へ送信できたときの処理(10を送る)
default:
// どのチャネルも準備できていないとき(省略可)
}
最小の具体例で、送信側のcaseとdefaultの動きを確認してみましょう。バッファ付きチャネルを使うと、今この瞬間に送れるかどうかで分岐できます。
package main
import "fmt"
func main() {
ch := make(chan int, 1) // 1個だけ入るバッファ
select {
case ch <- 100:
fmt.Println("送信できました")
default:
fmt.Println("今は送信できません")
}
select {
case v := <-ch:
fmt.Println("受信:", v)
default:
fmt.Println("今は受信できません")
}
}
このサンプルでは、最初のselectで100の送信に成功し、次のselectでその値を受信します。selectはこのように「今すぐ進める処理を安全に選ぶ」ための基本構文として使います。
3. 実際にselect文を使ったサンプルを見てみよう
実際に、2つのチャネルを使って、どちらか先にデータが来た方の処理をする例を見てみましょう。
package main
import (
"fmt"
"time"
)
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go func() {
time.Sleep(2 * time.Second)
ch1 <- "チャネル1からデータが届きました"
}()
go func() {
time.Sleep(1 * time.Second)
ch2 <- "チャネル2からデータが届きました"
}()
select {
case msg1 := <-ch1:
fmt.Println(msg1)
case msg2 := <-ch2:
fmt.Println(msg2)
}
}
このコードでは、ch1とch2という2つのチャネルがあります。1秒後にch2から、2秒後にch1からデータが届きます。
select文は、先に届いたch2のデータを受け取って、その処理を実行します。
実行結果:
チャネル2からデータが届きました
4. default文で待たずに処理する
select文にはdefault節を使うこともできます。
これは、どのチャネルにもデータが来ていないときに、すぐに別の処理をさせたい場合に使います。
select {
case msg := <-ch1:
fmt.Println(msg)
default:
fmt.Println("まだデータが来ていません")
}
この例では、チャネルch1からデータが来ていなければ、すぐに"まだデータが来ていません"と表示されます。
5. select文を使うときのポイント
- 1つでもcaseのチャネルにデータがあれば、select文はすぐに動く
- すべてのチャネルにデータがない場合は、select文は待機状態になる
- default節があると、待たずにすぐに実行される
この仕組みを使えば、複数の並行処理の中から、「一番早く終わった処理」を優先して動かすことができるのです。
6. 実生活で例えるなら?
イメージしやすくするために、select文を「電話の呼び出し」に例えてみましょう。
あなたが2つの電話を持っていて、どちらかが先に鳴ったら、そちらに出る…というのがselectの動きです。
どちらも鳴らなければ待ち続けるし、「電話が鳴らなかったらとりあえずテレビを見る」と決めているなら、それがdefaultです。
7. select文はどんな場面で役立つの?
ネットワーク通信やタイマー処理、ユーザー入力の待ち受けなど、さまざまな処理を同時に扱う場面でselect文は大活躍します。
たとえば、「3秒以内にサーバーから返事がなければタイムアウトする」という処理も、selectを使えば簡単に書けるのです。
まとめ
学んだポイントの総整理
ここまでで、Go言語のselect文が「複数のチャネルを同時に見張り、最初に準備できたものだけを安全に処理する仕組み」であることを確認できました。基本構文はとても素直で、caseに書けるのは送受信だけ、同時に複数が準備できた場合はその中からひとつだけが選ばれます。どれも準備できていないなら待機、ただしdefaultがあればすぐに先へ進めます。これだけで、並行処理の「待つ/進める」の判断が読みやすい形で記述できます。
また、goroutineで走る処理の結果や通知をchannel経由で受け取り、selectで先着順に振り分けるという作法を覚えると、タイマー、ユーザー入力、ネットワークの応答など「どちらが先かわからない出来事」をスムーズに扱えるようになります。特に、一定時間だけ待って諦める「タイムアウト」や、今は待たずに別処理に移る「default分岐」は実装の現場でとても重宝します。
書き方の型を身につける(最小の型)
まずは最小の型を手に馴染ませましょう。caseは受信か送信、defaultは「今すぐ動けるものが無い」時の逃げ道です。短いサンプルで、待つ/待たないの感覚を掴むのが近道です。
package main
import "fmt"
func main() {
done := make(chan struct{})
msg := make(chan string, 1)
// いま送れるなら送る(バッファに空きがあると送信可能)
select {
case msg <- "すぐに送れました":
fmt.Println("送信成功")
default:
fmt.Println("まだ送れません")
}
// いま受け取れるなら受信する(値が入っていれば即時)
select {
case m := <-msg:
fmt.Println("受信:", m)
case <-done:
fmt.Println("終了します")
default:
fmt.Println("何も準備できていません")
}
}
この型を出発点に、必要に応じてチャネルの数を増やしたり、分岐後の処理を関数に切り出したりすれば、規模が大きくなっても読みやすさを保ちやすくなります。
タイムアウトとリトライの基本パターン
現場で頻出するのが「一定時間だけ待ってから諦める」という分岐です。time.Afterが返すチャネルをselectに混ぜるだけで、難しい仕組みを用意しなくても明快に表現できます。下は簡単なリトライつきの型です。
package main
import (
"fmt"
"time"
)
func request() <-chan string {
ch := make(chan string, 1)
go func() {
// 実際はHTTPやDBなどの処理を想定
time.Sleep(1500 * time.Millisecond)
ch <- "応答OK"
}()
return ch
}
func main() {
const maxRetry = 2
for attempt := 1; attempt <= maxRetry; attempt++ {
select {
case res := <-request():
fmt.Println("成功:", res)
return
case <-time.After(1 * time.Second):
fmt.Println("タイムアウト、やり直します(試行", attempt, ")")
}
}
fmt.Println("失敗:規定回数内に応答がありませんでした")
}
このパターンを覚えておくと、基礎的なネットワーク待ち・ファイルI/Oの待ち・外部コマンドの待ちなどにそのまま応用できます。実務では、試行回数や待ち時間を定数化しておくと、テストや運用の調整が楽になります。
イベントループの型(繰り返し+停止シグナル)
複数の入力源を継続的に回し続けるときは、for + selectの組み合わせが定番です。停止はdoneチャネルを閉じるだけで簡潔に表現できます。下の例では、メッセージ処理・秒ごとの心拍(ハートビート)・停止命令の三者を同時に扱っています。
package main
import (
"fmt"
"time"
)
func main() {
msgs := make(chan string)
done := make(chan struct{})
// 入力源(例:別ゴルーチンで送られてくるメッセージ)
go func() {
msgs <- "こんにちは"
time.Sleep(300 * time.Millisecond)
msgs <- "処理中です"
time.Sleep(300 * time.Millisecond)
close(done) // 終了シグナル
}()
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case m := <-msgs:
fmt.Println("受信:", m)
case <-ticker.C:
fmt.Println("ハートビート")
case <-done:
fmt.Println("終了します")
return
}
}
}
このように書いておけば、どの入力が先でも安全に処理され、終了も素直に扱えます。後から処理を増やしてもcaseを足していくだけなので、構造が崩れにくいのも利点です。
読みやすさを保つ小さなコツ
分岐が増えるほど読み手にとっての負担は大きくなります。selectの各caseが長くなってきたら、関数に切り出す、ログの書式を揃える、チャネル名を用途で命名するといった地味な工夫が効きます。また、defaultは便利ですが、頻用しすぎると「常にdefaultばかり選ばれて忙しく回り続ける」状態にもなり得ます。必要な場面だけに絞ると、CPUの無駄な消費を抑えられます。
練習課題(手を動かして理解を固める)
次の小さな課題を手元で試すと、理解がぐっと定着します。①二つのチャネルの先着勝ちを表示する(基本)。②time.Afterで1秒タイムアウトを入れてみる(遅い側を諦める)。③for+selectで10回だけメッセージを処理し、終わったらdoneで停止する(イベントループ)。コードを短く保ちながら、名前やログの書式で「何を待っているか」が読み取れるように工夫してみましょう。
生徒
「selectって、たくさんの処理を同時に待つための『受付』みたいな感じなんですね。早く終わった受付だけ対応する、と。」
先生
「その通り。caseは受付窓口、チャネルは呼び出しベル。鳴った窓口だけ応対するイメージです。鳴らないならdefaultで次へ進めば、処理が止まらない。」
生徒
「time.Afterを混ぜれば、待ちすぎないでタイムアウトも書ける、と。実際のアプリでも使えそうです。」
先生
「よく使いますよ。ネットワークの応答待ち、バッチ処理の監視、UIの入力待ち……どれもselectの得意分野です。for+selectでループ化して、doneで止める型も覚えておくと、設計が安定します。」
生徒
「各caseが長くなって読みにくかったら、関数に切ればよいんですね。ログの形も揃えておけば、あとから読んでも流れが追いやすそう。」
先生
「うん。あとはdefaultの使いすぎに注意。常にdefaultが選ばれると忙しく回ってしまう。必要な場面だけにして、待つべきところはきちんと待つ。これがコツです。」
生徒
「わかりました。まずは先着勝ち、タイムアウト、イベントループの三つを練習して、selectの感覚を掴みます!」