Go言語のselect文、並行処理を書くときに出てくるけど、いまいち掴みどころがない…なんて思っていませんか?
この記事では、Go言語のパワフルな機能である`select`文について、基本的な仕組みから、複数のチャネルを華麗にさばく方法、タイムアウトの設定、さらにはハマりやすい落とし穴まで解説していきます。
読み終わるころには、`select`文と友達になれて、Goの並行処理コードを書くのがもっと楽しくなっているはず! さあ、一緒に見ていきましょう!
この記事で学べること
- `select`文が何をするものなのか
- `select`文の基本的な書き方
- 複数のチャネルからデータを受け取る方法
- 特定のチャネルにデータを送る方法
- 処理を止めずにチャネル操作する`default`の使い方
- 処理に時間制限をつけるタイムアウトの方法
- `select`文を使うときに気をつけること
Go言語のselect文とは?並行処理の「交通整理役」
Go言語といえば、並行処理、つまり同時にたくさんの処理をこなすのが得意ですよね。その並行処理をスムーズに進めるために活躍するのが`select`文なんです。
たくさんのゴルーチン(Go言語版の軽いスレッドみたいなもの)が、チャネルっていうデータの通り道を使ってやり取りするとき、どのチャネルの準備ができたかを見張って、最初に準備ができたチャネルの処理を実行してくれる、いわば並行処理界の交通整理役みたいな存在です。
もし`select`文がなかったら、どのチャネルが使える状態か一つ一つチェックしないといけなくて、コードがごちゃごちゃしたり、うっかり処理が止まっちゃったり(デッドロックって言います)するかもしれません。
`select`文は、そんな面倒ごとから私たちを救ってくれるヒーローなのです。
// イメージ 複数のチャネル(道路) ---> | select文 | ---> 最初に準備完了した チャネル1 (道路1) | (交通整理員) | チャネルの処理を実行 チャネル2 (道路2) | | チャネル3 (道路3) | | ... ---> | | --->
Go言語のselect文の基本的な書き方
`select`文の基本的な形はこんな感じです。
select { case <-チャネル1: // チャネル1から受信したときの処理 case データ := <-チャネル2: // チャネル2から受信して、データを使う処理 case チャネル3 <- 送るデータ: // チャネル3へデータを送信したときの処理 default: // どのcaseも準備できていないときの処理 (省略可能) }
見た目は`switch`文に似ていますね。
select
キーワードで始まります。case
の後に、チャネルの受信操作 (<-チャネル
) か送信操作 (チャネル <- データ
) を書きます。- それぞれの
case
ブロックには、そのチャネル操作が成功したときに実行したい処理を書きます。 - もし、複数の
case
のチャネルが同時に準備OKになった場合は、どれか一つがランダムに選ばれて実行されます。全部実行されるわけではないので注意です! default
は、どのcase
もすぐに実行できないときに実行されます。これは無くてもOKです。
Go言語のselect文の使い方
複数チャネルからの受信
一番よく使われるのが、複数のチャネルの中から、最初にデータを受信できるようになったチャネルを待つ、という使い方です。
例えば、チャネルAとチャネルBがあって、どちらから先にデータが送られてくるか分からない…という状況で、`select`文を使うと、先にデータが来た方の処理を実行できます。早い者勝ちってことですね!
実際にコードを見てみましょう。2つのチャネルを用意して、別々のゴルーチンから、ちょっと時間をずらしてデータを送ってみます。
package main import ( "fmt" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) // ゴルーチン1: 1秒後にch1へ送信 go func() { time.Sleep(1 * time.Second) ch1 <- "データ1が来たよ!" }() // ゴルーチン2: 2秒後にch2へ送信 go func() { time.Sleep(2 * time.Second) ch2 <- "データ2が来たよ!" }() fmt.Println("どっちのチャネルが先に来るかな…?") // selectで待機 select { case msg1 := <-ch1: fmt.Println("ch1から受信:", msg1) case msg2 := <-ch2: fmt.Println("ch2から受信:", msg2) } fmt.Println("処理が終わったよ。") }
実行結果は、おそらくこうなります(ch1の方が早くデータが来るので)。
どっちのチャネルが先に来るかな…? ch1から受信: データ1が来たよ! 処理が終わったよ。
もし、ゴルーチン1と2の`time.Sleep`の時間を逆にすれば、ch2から受信する結果になります。場合によっては、タイミング次第でどちらが実行されるか変わることもある、という点は覚えておいてくださいね。
このサンプルコードが何をしているか、もう少し詳しく見てみましょう。
make(chan string)
で、文字列を送受信できるチャネルch1
とch2
を作っています。go func() { ... }()
で、2つのゴルーチンを起動しています。ゴルーチンはメインの処理とは別に、裏側で動いてくれる働き者です。- 1つ目のゴルーチンは、1秒待ってから
ch1
に文字列データを送信します (ch1 <- "データ1が来たよ!"
)。 - 2つ目のゴルーチンは、2秒待ってから
ch2
に文字列データを送信します (ch2 <- "データ2が来たよ!"
)。 - メインの処理では、
select
文がch1
からの受信とch2
からの受信を同時に待ち構えます。 - 今回は
ch1
の方が早く送信されるので、select
文はcase msg1 := <-ch1:
のブロックを実行します。受信したデータはmsg1
に入ります。 select
文はどれか一つのcase
を実行したら終了するので、ch2
からの受信を待たずに次の処理に進みます。
このように、Goroutineとチャネルが連携して非同期にデータを送り、`select`がそれを受け取る準備ができるのを待つ、というのが基本的な流れです。
チャネルへの送信
受信だけでなく、複数のチャネルの中から送信できるチャネルを選ぶ、という使い方もできます。
例えば、チャネルXとチャネルYがあって、どちらかのチャネルがデータを受け取れる状態になったら、そっちにデータを送りたい、という場合に役立ちます。
select { case chX <- データ: fmt.Println("チャネルXに送信成功!") case chY <- データ: fmt.Println("チャネルYに送信成功!") }
どのcase
が実行されるかは、相手側のチャネルがデータを受け取れる状態 (<- chX
のような受信待ちの状態) になっているかどうかで決まります。
ノンブロッキング操作 (default)
`select`文には、特別なdefault
というケースを追加できます。
select { case msg := <-ch: fmt.Println("受信:", msg) default: fmt.Println("今は受信できるデータがないみたい。") // 他の処理をするなど }
default
ケースがあると、select
文は他のどのcase
のチャネル操作もすぐに実行できない場合に、default
ブロックを実行します。そして、待たずにすぐに次の処理に進みます。これをノンブロッキング操作といいます。
もしdefault
がなくて、どのチャネルも準備ができていなかったら、準備ができるまで`select`文はずーっと待ち続けます(ブロックするといいます)。
default
は、例えばこんな時に便利です。
- チャネルにデータが来ていれば処理したいけど、来ていなければ別の作業を進めたいとき (ポーリング)
- メインの処理の合間に、もしチャネルに何か来てたら対応したいとき
`default` を使うと処理が止まらなくなるので、プログラム全体の応答性を良くしたい場合に便利です。
タイムアウト処理 (time.After)
チャネル操作をするとき、「一定時間待ってもデータが来なかったら、諦めて別の処理をしたい」なんて思うこと、ありますよね。そんなときに使えるのが、`select`文と`time`パッケージの合わせ技です。
time.After(時間)
という関数を使うと、指定した時間が経過した後に現在時刻を送信してくる特殊なチャネルを作れます。これを`select`文の`case`の一つとして使うのです。
select { case res := <-処理結果チャネル: fmt.Println("処理完了:", res) case <-time.After(5 * time.Second): // 5秒待つ fmt.Println("時間切れ!タイムアウトしました。") }
このコードでは、
処理結果チャネル
からデータを受信できるか- 5秒経過するか
のどちらか早い方が起こるのを待ちます。もし5秒以内に処理結果チャネル
からデータが来れば、その処理が実行されます。もし5秒経ってもデータが来なければ、time.After
のチャネルが受信可能になり、タイムアウト処理が実行される、という仕組みです。
これで、一定時間待ってダメなら諦める処理が書けるようになります。ネットワーク通信とか、時間のかかる可能性のある処理を扱うときに非常に役立ちますね。
タイムアウトする例を見てみましょう。
package main import ( "fmt" "time" ) func main() { ch := make(chan string, 1) // バッファを1に // わざと時間のかかる処理(今回は何もしない) go func() { // time.Sleep(3 * time.Second) // 3秒かかる想定 // ch <- "処理終わったよ!" // でも結果を返さない fmt.Println("ゴルーチンは仕事したフリ...") }() fmt.Println("処理結果を待つよ... (2秒以内に来ないとタイムアウト)") select { case res := <-ch: fmt.Println("結果受信:", res) case <-time.After(2 * time.Second): // 2秒でタイムアウト fmt.Println("タイムアウト!もう待てないよ!") } fmt.Println("メイン処理終了。") }
実行結果
処理結果を待つよ... (2秒以内に来ないとタイムアウト) ゴルーチンは仕事したフリ... タイムアウト!もう待てないよ! メイン処理終了。
今度は、タイムアウトする前に処理が終わる例です。上記のコードのコメントアウト部分を有効にしてみましょう。
package main import ( "fmt" "time" ) func main() { ch := make(chan string, 1) go func() { time.Sleep(1 * time.Second) // 1秒で終わる! ch <- "処理終わったよ!" // 結果を返す fmt.Println("ゴルーチンは仕事した!") }() fmt.Println("処理結果を待つよ... (2秒以内に来ないとタイムアウト)") select { case res := <-ch: fmt.Println("結果受信:", res) case <-time.After(2 * time.Second): // 2秒でタイムアウト fmt.Println("タイムアウト!もう待てないよ!") } fmt.Println("メイン処理終了。") }
実行結果
処理結果を待つよ... (2秒以内に来ないとタイムアウト) ゴルーチンは仕事した! 結果受信: 処理終わったよ! メイン処理終了。
タイムアウトの仕組みは、select
文が複数の可能性を同時に待てる、という性質を利用しています。
time.After(設定時間)
は、指定時間が経つと時刻データを送ってくる特別な読み取り専用チャネルを返します。これを「タイマーチャネル」と呼ぶことにしましょう。select
文は、通常の作業用チャネル(例:ch
)からの受信と、このタイマーチャネルからの受信を同時に待ちます。- もし設定時間内に作業用チャネルからデータが届けば、そちらの
case
が実行されます。めでたしめでたし。 - もし設定時間内に作業用チャネルからデータが来なければ、タイマーチャネルの方が先に受信可能になります。すると、タイマーチャネルを待っていた
case
が実行され、タイムアウトしたことが分かります。
`time.After` が時間切れチャネルを用意してくれるおかげで、`select`文だけで簡単にタイムアウト処理が書ける、というわけですね。
Go言語のselect文を使う上での注意点
`select`文は便利ですが、いくつか気をつけておきたい点があります。これを知らないと、予期せぬ動きに悩まされるかも…
nilチャネルに注意!
初期化されていない(nil
の)チャネルに対する送受信操作は、永遠にブロックします。つまり、case
にnil
チャネルを書くと、そのcase
は絶対に選ばれなくなります。select
文の全てのcase
がnil
チャネルの操作で、かつdefault
もなければ、プログラムはそこで永久に停止(デッドロック)してしまいます!意図しないnil
チャネルを作らないように気をつけましょう。デッドロックの可能性
前述のnil
チャネルのケースもそうですが、default
ケースがなく、かつ全てのcase
のチャネル操作が(相手がいなくて)実行できない状態になると、`select`文は永遠に待ち続けてデッドロックとなります。select
しかない、といった状況です。forループとの組み合わせ
for
ループの中で`select`を使うこともよくあります。val, ok := <-ch
の ok
が false
なら閉じられている)。【まとめ】Go言語 selectを使いこなして並行処理をレベルアップ!
いやー、`select`文、なかなか奥が深いけど面白いヤツでしたね!
最後に、今回学んだことのポイントをおさらいしておきましょう。
- `select`文は、複数のチャネル操作の中から最初に準備できたものを実行する「交通整理役」。
- 基本的な書き方は
select { case 操作1: ... case 操作2: ... }
。 - 複数チャネルからの受信や、送信可能なチャネルへの送信に使える。
default
ケースで、待たずに進むノンブロッキング操作ができる。time.After
と組み合わせて、タイムアウト処理を実装できる。nil
チャネルやデッドロックには注意が必要!
`select`文をマスターすれば、Go言語の得意な並行処理を、より柔軟に、より安全に書けるようになります。ゴルーチンとチャネルを使ったコードが、もっとパワフルになりますよ!
さあ、どんどん`select`文を使って、Goプログラミングを楽しんでいきましょう!
【関連記事】 Go言語とは?特徴・できること・将来性
0 件のコメント:
コメントを投稿
注: コメントを投稿できるのは、このブログのメンバーだけです。