selectを使用して効果的なGo並行処理
Min-jun Kim
Dev Intern · Leapcell

Preface
Goプログラミング言語では、GoroutineとChannelは並行プログラミングにおける重要な概念です。これらは並行処理に関連する様々な問題を解決するのに役立ちます。この記事では、複数のチャネルを調整するための架け橋として機能するselect
に焦点を当てます。
Introduction to select
What is select
select
は、複数の通信操作の中から実行可能な操作を選択するためにGoで使用される制御構造です。複数のチャネルでの読み取りおよび書き込み操作を調整し、複数のチャネルを介した非ブロッキングデータ伝送、同期、および制御を可能にします。
Why Do We Need select
Goのselect
ステートメントは、チャネルを多重化するメカニズムを提供します。複数のチャネルでメッセージを待機して処理できます。単にfor
ループを使用してチャネルを反復処理するよりも、select
は複数のチャネルを管理するためのより効率的な方法です。
select
を使用する一般的なシナリオをいくつか示します。
-
複数のチャネルからのメッセージを待機(多重化) 複数のチャネルからのメッセージを待機する必要がある場合、
select
を使用すると、いずれかがデータを受信するのを待機するのが便利になり、同期と待機のために複数のGoroutineを使用する必要がなくなります。 -
チャネルメッセージのタイムアウト待機 特定の期間内にチャネルからのメッセージを待機する必要がある場合、
select
をtime
パッケージと組み合わせて、時間指定された待機を実装できます。 -
チャネルでの非ブロッキング読み取り/書き込み チャネルにデータまたはスペースがない場合、チャネルからの読み取りまたはチャネルへの書き込みはブロックされます。
default
ブランチでselect
を使用すると、非ブロッキング操作が可能になり、デッドロックや無限ループが回避されます。
したがって、select
の主な目的は、複数のチャネルを処理するための効率的で使いやすいメカニズムを提供し、Goroutineの同期と待機を簡素化し、プログラムをより読みやすく、効率的で、信頼性の高いものにすることです。
Basics of select
Syntax
select { case <- channel1: // channel1 is ready case data := <- channel2: // channel2 is ready, and data can be read case channel3 <- data: // channel3 is ready, and data can be written into it default: // no channel is ready }
ここで、<- channel1
はchannel1
からの読み取りを意味し、data := <- channel2
はdata
へのデータ受信を意味します。channel3 <- data
はdata
をchannel3
に書き込むことを意味します。
select
の構文はswitch
に似ていますが、チャネル操作にのみ使用されます。select
ステートメントでは、複数のcase
ブロックを定義できます。各ブロックは、データの読み取りまたは書き込みのためのチャネル操作です。複数のケースが同時に準備完了している場合、1つがランダムに選択されます。準備ができていない場合、default
ブランチ(存在する場合)が実行されます。それ以外の場合、少なくとも1つのケースが準備完了になるまでselect
はブロックされます。
Basic Usage
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) ch2 := make(chan int) go func() { time.Sleep(1 * time.Second) ch1 <- 1 }() go func() { time.Sleep(2 * time.Second) ch2 <- 2 }() for i := 0; i < 2; i++ { select { case data, ok := <-ch1: if ok { fmt.Println("Received from ch1:", data) } else { fmt.Println("Channel closed") } case data, ok := <-ch2: if ok { fmt.Println("Received from ch2:", data) } else { fmt.Println("Channel closed") } } } select { case data, ok := <-ch1: if ok { fmt.Println("Received from ch1:", data) } else { fmt.Println("Channel closed") } case data, ok := <-ch2: if ok { fmt.Println("Received from ch2:", data) } else { fmt.Println("Channel closed") } default: fmt.Println("No data received, default branch executed") } }
Execution Result
Received from ch1: 1
Received from ch2: 2
No data received, default branch executed
上記の例では、2つのチャネルch1
とch2
が作成されます。別々のGoroutineが、異なる遅延の後でこれらのチャネルに書き込みます。メインのGoroutineは、select
ステートメントを使用して両方のチャネルをリッスンします。データがチャネルに到着すると、データが出力されます。ch1
がch2
よりも先にデータを受信するため、メッセージ"Received from ch1: 1"
が最初に出力され、その後に"Received from ch2: 2"
が出力されます。
default
ブランチを示すために、プログラムには2番目のselect
ブロックが含まれています。この時点で、ch1
とch2
は両方とも空であるため、default
ブランチが実行され、"No data received, default branch executed"
が出力されます。
Scenarios Combining select
and Channels
Implementing Timeout Control
package main import ( "fmt" "time" ) func main() { ch := make(chan int) go func() { time.Sleep(3 * time.Second) ch <- 1 }() select { case data, ok := <-ch: if ok { fmt.Println("Received data:", data) } else { fmt.Println("Channel closed") } case <-time.After(2 * time.Second): fmt.Println("Timed out!") } }
Execution Result: Timed out!
この例では、プログラムは3秒後にch
チャネルにデータを送信します。ただし、select
ブロックは2秒のタイムアウトを設定します。その時間内にデータが受信されない場合、タイムアウトケースがトリガーされます。
Implementing Multi-Task Concurrent Control
package main import ( "fmt" ) func main() { ch := make(chan int) for i := 0; i < 10; i++ { go func(id int) { ch <- id }(i) } for i := 0; i < 10; i++ { select { case data, ok := <-ch: if ok { fmt.Println("Task completed:", data) } else { fmt.Println("Channel closed") } } } }
Execution Result (実行順序は実行ごとに異なる場合があります):
Task completed: 1
Task completed: 5
Task completed: 2
Task completed: 3
Task completed: 4
Task completed: 0
Task completed: 9
Task completed: 6
Task completed: 7
Task completed: 8
この例では、10個のGoroutineが起動されてタスクを同時に実行します。単一のチャネルを使用して、タスク完了通知を受信します。メイン関数はselect
を使用してこのチャネルをリッスンし、受信時に完了した各タスクを処理します。
Listening to Multiple Channels
package main import ( "fmt" "time" ) func main() { ch1 := make(chan int) ch2 := make(chan int) // Start Goroutine 1 to send data to ch1 go func() { for i := 0; i < 5; i++ { ch1 <- i time.Sleep(time.Second) } }() // Start Goroutine 2 to send data to ch2 go func() { for i := 5; i < 10; i++ { ch2 <- i time.Sleep(time.Second) } }() // Main Goroutine receives and prints data from ch1 and ch2 for i := 0; i < 10; i++ { select { case data := <-ch1: fmt.Println("Received from ch1:", data) case data := <-ch2: fmt.Println("Received from ch2:", data) } } fmt.Println("Done.") }
Execution Result (実行順序は実行ごとに異なる場合があります):
Received from ch2: 5
Received from ch1: 0
Received from ch1: 1
Received from ch2: 6
Received from ch1: 2
Received from ch2: 7
Received from ch1: 3
Received from ch2: 8
Received from ch1: 4
Received from ch2: 9
Done.
この例では、select
を使用すると、複数のチャネルからのデータを多重化できます。これにより、プログラムは同期のために別々のGoroutineを必要とせずに、ch1
とch2
を同時にリッスンできます。
Using default
to Achieve Non-blocking Read and Write
import ( "fmt" "time" ) func main() { ch := make(chan int, 1) go func() { for i := 1; i <= 5; i++ { ch <- i time.Sleep(1 * time.Second) } close(ch) }() for { select { case val, ok := <-ch: if ok { fmt.Println(val) } else { ch = nil } default: fmt.Println("No value ready") time.Sleep(500 * time.Millisecond) } if ch == nil { break } } }
Execution Result (実行順序は実行ごとに異なる場合があります):
No value ready
1
No value ready
2
No value ready
No value ready
3
No value ready
No value ready
4
No value ready
No value ready
5
No value ready
No value ready
このコードは、default
ブランチを使用して、非ブロッキングチャネルの読み取りと書き込みを実装します。select
ステートメントでは、チャネルが読み取りまたは書き込みの準備ができている場合、対応するブランチが実行されます。チャネルの準備ができていない場合、default
ブランチが実行され、ブロッキングが回避されます。
Notes on Using select
select
を使用する際に留意すべき重要な点を次に示します。
select
ステートメントは、チャネルからの読み取りまたはチャネルへの書き込みなど、通信操作にのみ使用できます。通常の計算や関数呼び出しには使用できません。select
ステートメントは、少なくとも1つのケースが準備できるまでブロックされます。複数のケースが準備できている場合、1つがランダムに選択されます。- 準備ができているケースがなく、
default
ブランチが存在する場合、default
ブランチはすぐに実行されます。 select
でチャネルを使用する場合は、チャネルが適切に初期化されていることを確認してください。- チャネルが閉じられている場合でも、空になるまで読み取ることができます。閉じられたチャネルからの読み取りは、要素型のゼロ値と、チャネルの閉じられたステータスを示すブール値を返します。
要約すると、select
を使用する場合は、デッドロックやその他の問題を回避するために、各ケースの条件と実行順序を慎重に検討してください。
We are Leapcell, your top choice for hosting Go projects.
Leapcell is the Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis:
Multi-Language Support
- Develop with Node.js, Python, Go, or Rust.
Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the Documentation!
Follow us on X: @LeapcellHQ