Day 011 — select文

2026-08-08 🔵 䞭玚者 / Phase 2 実装 select文

📚 背景知識読んでから問題ぞ

前回、1぀のchannelに察しお送信・受信を行う基本を孊びたした。しかし実務では「耇数のchannelのうち、どれか1぀でも倀が来たらそれを凊理したい」ずいう堎面が頻繁に発生したす。それを実珟するのがselect文です。

select {
case v := <-ch1:
    // ch1から受信できた堎合
case v := <-ch2:
    // ch2から受信できた堎合
default:
    // どのcaseもreadyでない堎合即座に実行される
}

selectはswitchに䌌た構文ですが、比范察象は倀ではなくchannel操䜜の実行可吊です。挙動のルヌルは次の通りです。

  • 耇数のcaseのうち、readyな送受信可胜なものがあればそれを実行する
  • 耇数のcaseが同時にreadyな堎合は、擬䌌ランダムに1぀が遞ばれる先頭に曞いたcaseが優先されるわけではない
  • どのcaseもreadyでない堎合、defaultがあれば即座にdefaultを実行ブロックしない。defaultがなければ、いずれかのcaseがreadyになるたでブロックする

selectが真䟡を発揮するのが、time.Afterず組み合わせたタむムアりト凊理です。

select {
case v := <-ch:
    fmt.Println("受信:", v)
case <-time.After(3 * time.Second):
    fmt.Println("タむムアりト")
}

time.After(d)は、d経過埌に珟圚時刻を1぀送信する<-chan Timeを返す関数です。これをselectのcaseに含めるこずで、「chから倀が来るのを埅぀が、䞀定時間埅っおも来なければ諊める」ずいう凊理が、远加のgoroutineやフラグ倉数なしにたった数行で曞けたす。これはGoが「タむムアりト」ずいう頻出パタヌンを蚀語プリミティブレベルで自然に衚珟できる奜䟋です。

もう1぀重芁な性質ずしお、selectのcase数はコンパむル時に固定されるずいう制玄がありたす。動的な数のchannel䟋:スラむスで枡されたN個のchannelをselectで同時に監芖したい堎合は、暙準のselect文では曞けず、reflect.Selectずいう別の仕組みが必芁になりたすこれはPhase 3以降で扱う応甚テヌマです。今回はcase数が固定の基本圢に絞っお扱いたす。

📝 問題

以䞋の芁件を満たす、1぀の実行可胜なGoプログラムを実装しおください。

  1. firstResponse(ch1, ch2, ch3 <-chan string, timeout time.Duration) (string, error) ずいう関数を実装しおください。この関数はselect文で3぀のchannelを同時に監芖し、最初にどれか1぀から届いた倀をそのたた返しおください。どのchannelからも倀が来ないたたtimeoutが経過した堎合は、空文字列ずerrors.Newで䜜成した゚ラヌを返しおくださいtime.Afterを䜿うこず
  2. mainでは、worker(label string, delay time.Duration) <-chan stringずいうヘルパヌ関数goroutineを起動し、delay埌にlabelを送信するchannelを返すを甚意し、遅延の異なる3぀のworkerを䜜っおfirstResponseを呌び出し、最速で応答したworkerのlabelが衚瀺されるこずを確認しおください
  3. 3぀のworkerすべおがtimeoutより遅く応答するケヌスも甚意し、firstResponseが正しくタむムアりト゚ラヌを返すこずを確認しおください
  4. 同じプログラム内に、selectのdefault節を䜿ったノンブロッキングなポヌリングの䟋も実装しおください䞀定回数、短い間隔でchannelを確認し、ただ倀がなければ「ただ届いおいたせん」ずいったログを出し、倀が届いたらそこでポヌリングをやめる

go run main.goでそのたた実行できる、完党な1぀のプログラムずしお提出しおください。

🔍 ヒント段階的開瀺

ヒント1 — 方向性

selectは耇数のcaseのうちreadyなものを実行する構文です。3぀のchannel受信ず1぀のtime.Afterをcaseずしお䞊べれば、「3぀のうちどれかが来るたで埅぀が、䞀定時間経ったら諊める」ずいう凊理を1぀のselectブロックだけで衚珟できたす。default節があるselectはブロックしない点、default節がないselectは必ずどれかのcaseがreadyになるたでブロックする点の違いを意識しおください。

ヒント2 — アプロヌチ

worker関数は前回孊んだgoroutine+channelのパタヌンそのものです。バッファ1のchannelを䜿うず、受信偎が受け取らなくおもgoroutineが送信でブロックされたたた残り続ける心配が少なくなりたす今回は必ず受信するか、timeoutで芋捚おるかのどちらかですが、goroutineリヌクを避ける蚭蚈を意識する癖を぀けたしょう。ノンブロッキングポヌリングは、forルヌプの䞭にdefault節぀きのselectを眮き、default偎でtime.Sleepを挟んで少し埅っおから次のポヌリングに進む圢になりたす。

ヒント3 — コヌド骚栌
package main

import (
	"errors"
	"fmt"
	"time"
)

func firstResponse(ch1, ch2, ch3 <-chan string, timeout time.Duration) (string, error) {
	select {
	case v := <-ch1:
		return v, nil
	case v := <-ch2:
		return v, nil
	case v := <-ch3:
		return v, nil
	case <-time.After(timeout):
		return "", errors.New("timeout")
	}
}

func worker(label string, delay time.Duration) <-chan string {
	ch := make(chan string, 1)
	go func() {
		time.Sleep(delay)
		ch <- label
	}()
	return ch
}

func main() {
	ch1 := worker("worker-A", 300*time.Millisecond)
	ch2 := worker("worker-B", 100*time.Millisecond)
	ch3 := worker("worker-C", 500*time.Millisecond)

	result, err := firstResponse(ch1, ch2, ch3, 1*time.Second)
	fmt.Println(result, err)

	// ここから、timeoutケヌスずdefault節によるポヌリングを远加しおいく
}

✅ 暡範解答

package main

import (
	"errors"
	"fmt"
	"time"
)

// firstResponse は3぀のchannelをselectで同時に監芖し、最初に届いた倀を返す。
// どのchannelからも倀が来ないたたtimeoutが経過した堎合ぱラヌを返す。
func firstResponse(ch1, ch2, ch3 <-chan string, timeout time.Duration) (string, error) {
	select {
	case v := <-ch1:
		return v, nil
	case v := <-ch2:
		return v, nil
	case v := <-ch3:
		return v, nil
	case <-time.After(timeout):
		return "", errors.New("timeout: どのworkerからも応答がありたせんでした")
	}
}

// worker は指定した遅延の埌、labelをchannelぞ送信するgoroutineを起動し、
// 受信専甚channelを返す。
func worker(label string, delay time.Duration) <-chan string {
	ch := make(chan string, 1)
	go func() {
		time.Sleep(delay)
		ch <- label
	}()
	return ch
}

func main() {
	fmt.Println("=== 耇数workerのうち最速の応答を埅぀ ===")
	ch1 := worker("worker-A", 300*time.Millisecond)
	ch2 := worker("worker-B", 100*time.Millisecond)
	ch3 := worker("worker-C", 500*time.Millisecond)

	result, err := firstResponse(ch1, ch2, ch3, 1*time.Second)
	if err != nil {
		fmt.Println("゚ラヌ:", err)
	} else {
		fmt.Println("最速で応答したのは:", result)
	}

	fmt.Println("\n=== timeoutケヌス党workerがtimeoutより遅い ===")
	slowCh1 := worker("slow-A", 2*time.Second)
	slowCh2 := worker("slow-B", 2*time.Second)
	slowCh3 := worker("slow-C", 2*time.Second)
	_, err = firstResponse(slowCh1, slowCh2, slowCh3, 300*time.Millisecond)
	if err != nil {
		fmt.Println("゚ラヌ:", err)
	}

	fmt.Println("\n=== default節によるノンブロッキング・ポヌリング ===")
	pollCh := worker("poll-result", 250*time.Millisecond)
	received := false
	for i := 0; i < 5 && !received; i++ {
		select {
		case v := <-pollCh:
			fmt.Println("ポヌリング䞭に受信:", v)
			received = true
		default:
			fmt.Printf("  ただ届いおいたせん、ポヌリング%d回目\n", i+1)
			time.Sleep(100 * time.Millisecond)
		}
	}
	if !received {
		fmt.Println("  ポヌリング終了たでに受信できたせんでした")
	}
}
▶ 実行結果を芋るgo run main.go / 実際に怜蚌枈み
=== 耇数workerのうち最速の応答を埅぀ ===
最速で応答したのは: worker-B

=== timeoutケヌス党workerがtimeoutより遅い ===
゚ラヌ: timeout: どのworkerからも応答がありたせんでした

=== default節によるノンブロッキング・ポヌリング ===
  ただ届いおいたせん、ポヌリング1回目
  ただ届いおいたせん、ポヌリング2回目
  ただ届いおいたせん、ポヌリング3回目
ポヌリング䞭に受信: poll-result

※ goroutineのスケゞュヌリングにより现かいタむミングは実行のたびに倉わりたすが、傟向は毎回同じです。

🪜 Step-by-Step 解説

1
selectが耇数channelのうち最速のものを遞ぶ仕組み
firstResponse内のselectは、ch1・ch2・ch3のうち最初にreadyになったcaseを実行したす。䞊の実行䟋ではworker-Bが100ms、worker-Aが300ms、worker-Cが500msで応答するため、ch2が最初にreadyになり、そのcaseのreturn v, nilが実行されお他のcaseは評䟡されずに関数を抜けたす。ただ受信されなかったch1・ch3ぞの送信は、バッファ1のchannelのおかげでgoroutine偎がブロックされずに枈み、静かに終了したすバッファなしだった堎合、受け取られなかった送信偎goroutineは氞遠にブロックされたたた残っおしたいたすgoroutineリヌク。
2
time.Afterによるタむムアりトの実珟
2぀目の呌び出しでは、3぀のworkerすべおが2秒埌に応答するのに察しtimeoutは300msです。selectの4぀目のcaseであるcase <-time.After(timeout)が先にreadyになるため、こちらが遞ばれお゚ラヌが返りたす。ポむントは、time.Afterが返す<-chan Timeも他のchannelずたったく同列に扱われる、぀たり「timeoutもchannel経由のむベントの1぀」ずしお統䞀的に衚珟されるこずです。他の蚀語のようにtry/catch+タむマヌ割り蟌みのような特別な仕組みは䞍芁です。
3
default節によるノンブロッキング・ポヌリング
select {
case v := <-pollCh:
	received = true
default:
	time.Sleep(100 * time.Millisecond)
}
default節があるため、このselectはpollChにただ倀がなくおも絶察にブロックしたせん。readyでなければ即座にdefaultに萜ちおtime.Sleepで少し埅ち、次のルヌプぞ進みたす。これは「倀があれば凊理し、なければ他の仕事をするたたは少し埅っお再詊行する」ずいう、ブロッキングを避けたいシチュ゚ヌションで䜿われる定石パタヌンです。今回は250ms埌に届く倀を100ms間隔でポヌリングしおいるため、3回の「ただ届いおいたせん」の埌、4回目のルヌプで受信に成功したす。

💡 蚭蚈思想・なぜこう曞くのか

📌
耇数の入力源を埅぀こずを䞀玚垂民ずしお扱う: selectがGoに蚀語組み蟌みで甚意されおいるのは、Goが䞊行凊理においお「耇数の入力源を同時に埅぀」ずいう操䜜を䞀玚垂民ずしお扱っおいるからです。他の蚀語では、耇数の非同期凊理のうちどれかの完了を埅぀には、FutureやPromiseをラップする専甚APIや、コヌルバックの合成が必芁になりがちです。Goではselectずいう1぀の構文が、channelずいう統䞀されたむンタヌフェヌスの䞊で「タむムアりト」「耇数゜ヌスの倚重化」「ノンブロッキングな確認」のすべおを衚珟できたす。
📌
時間の経過もchannelむベントずしお統䞀的に扱う: 特にtime.Afterをselectのcaseにそのたた䞊べられる蚭蚈は象城的です。「䞀定時間埅぀」ずいう時間の経過も、Goでは特別扱いせずchannelから倀が届くずいう1぀のむベントずしお統䞀的にモデル化されおいたす。この䞀貫性が、Goの䞊行凊理コヌドが他蚀語に比べお短く読みやすくなる倧きな理由の1぀です。䞀方で、selectのcase数がコンパむル時に固定されるずいう制玄は、Goが「実行時の柔軟性」より「コンパむル時の明快さ・型安党性」を優先する蚭蚈思想の衚れでもありたす。

🌐 他蚀語ずの比范

芳点GoJavaPythonJavaScript/Node.js
盞圓する仕組みselect文蚀語組み蟌み構文CompletableFuture.anyOf(...)asyncio.wait(..., return_when=FIRST_COMPLETED)Promise.race([...])
タむムアりト付き埅機selectのcaseにtime.Afterを䞊べるfuture.get(timeout, unit)asyncio.wait_for(coro, timeout)Promise.race([p, timeoutPromise])timeout甹Promiseを自前で甚意
耇数readyな堎合の遞ばれ方擬䌌ランダムに1぀遞択公平性を保蚌実装䟝存通垞は最初に完了したもの完了した党タスクの集合が返る最初にresolve/rejectしたものが勝぀
ノンブロッキング確認default節぀きselectfuture.isDone()などのポヌリングメ゜ッドtask.done()などのポヌリングメ゜ッド基本的にコヌルバック/むベント駆動で、同期的なノンブロッキング確認ずいう抂念自䜓が薄い

Promise.raceやCompletableFuture.anyOfを䜿ったこずがある人にずっおは、「耇数の非同期凊理のうち最速のものを埅぀」ずいう発想自䜓は銎染みやすいはずです。違いは、Goではtime.Afterによるタむムアりトも同じselect構文の䞭に察等なcaseずしお曞ける点で、他蚀語のように「本䜓の凊理」ず「タむムアりト甚の特別なAPI」を別々に組み合わせる必芁がありたせん。

🏆 実務での䜿いどころ

  • 倖郚APIぞのタむムアりト付きリク゚スト: 耇数のレプリカ/フォヌルバック先に同時にリク゚ストを送り、最初に返っおきた応答を採甚し぀぀、䞀定時間で諊めるパタヌン今回のfirstResponseはこの簡易版
  • graceful shutdown: メむンの凊理channelず、OSシグナルを受け取るchannelos.Signalをselectで同時に監芖し、シグナルを受けたら安党に終了凊理ぞ移る実装は本番サヌビスの定番パタヌン
  • サヌキットブレヌカヌやリトラむ機構: context.Done()内郚的にはchannelず凊理完了channelをselectで監芖し、キャンセルず正垞完了のどちらが先に起きたかで分岐する
  • むベントルヌプ的な倚重化: 耇数のむベント゜ヌスタむマヌ、倖郚からの通知、内郚キュヌを1぀のselectルヌプで捌く、単玔だが堅牢なディスパッチャの実装

⚠ よくある誀解・ミス

誀解・ミスなぜ起こるか正しい理解
selectは先頭に曞いたcaseが優先されるず思い蟌むswitchやif-else ifのような「䞊から順に評䟡」ずいう感芚を持ち蟌む耇数のcaseが同時にreadyな堎合、Goは擬䌌ランダムに1぀を遞ぶ。特定のcaseを優先したい堎合はネストしたselectなど別の蚭蚈が必芁
default節がないselectもノンブロッキングだず思い蟌むselectずいう名前やdefaultの存圚を混同するdefault節がある堎合のみノンブロッキングになる。defaultがないselectは、いずれかのcaseがreadyになるたで通垞のchannel操䜜ず同様にブロックする
ルヌプの䞭で毎回time.Afterを呌んでしたうタむムアりト凊理を関数化する際、安易にforルヌプの内偎にtime.Afterを曞いおしたうtime.Afterはルヌプのたびに新しいタむマヌを生成し、発火するたでGCされない。ルヌプ内で繰り返し䜿う堎合はtime.NewTimerを1回だけ生成し、Stop()やリセットで䜿い回す蚭蚈が掚奚される
バッファなしchannelをworkerに䜿い、受信されなかった堎合にgoroutineがブロックされ続けるこずに気づかない「送信したらそれで終わり」ずいう感芚で実装しおしたうselectで耇数channelのうち1぀しか受信しない蚭蚈では、受け取られない送信偎goroutineが残る可胜性がある。バッファ付きchannelにする、contextでキャンセルを䌝える等の察策が必芁

🚀 次のステップ

  • 発展: firstResponseのtimeoutをtime.Afterではなくcontext.WithTimeoutで実装し盎しおみたしょう。ctx.Done()をselectのcaseに䜿う曞き方は、Phase 2の次のテヌマ「context パッケヌゞ」ぞの橋枡しになりたす
  • 次回予告: Day 012 — sync.WaitGroup実装問題。耇数のgoroutineの完了を、channelを䜿わずに埅ち合わせる暙準的な方法を孊びたす

🎯 自己評䟡

自分の回答

気づき・メモ