通信メカニズムとしてチャネルを使用する
Go のチャネルは、goroutine 間の通信メカニズムです。 Go のコンカレンシー手法は "メモリの共有で通信せず、通信でメモリを共有してください" というものでした。goroutine 間で値を送信する必要があるとき、チャネルを使用します。 そのしくみと、それらを使用して同時実行 Go プログラムを作成する方法を見てみましょう。
チャネルの構文
チャネルはデータを送受信する通信メカニズムであるため、型もあります。 つまり、チャネルがサポートする種類のデータのみを送信できます。 チャネルのデータ型としてキーワード chan
を使用しますが、int
型のように、チャネルを通過するデータ型も指定する必要があります。
チャネルを宣言するか、関数のパラメーターとしてチャネルを指定するたびに、chan int
のように chan <type>
を使用する必要があります。 チャネルを作成するには、次のような組み込みの make()
関数を使用します。
ch := make(chan int)
1 つのチャネルで、データの "送信" とデータの "受信" という 2 つの操作を実行できます。 チャネルに持たせる操作の種類を指定するには、チャネル演算子 <-
を使用する必要があります。 さらに、"チャネル内のデータ送信とデータ受信はブロック操作です"。 理由はすぐにわかります。
データの送信のみを行うチャネルであると指定する場合、チャネルの後に <-
演算子を使用します。 データを受信するチャネルにする場合、次の例のように、チャネルの前に <-
演算子を使用します。
ch <- x // sends (or writes ) x through channel ch
x = <-ch // x receives (or reads) data sent to the channel ch
<-ch // receives data, but the result is discarded
1 つのチャネルで使用できるもう 1 つの操作は、それを閉じることです。 チャネルを閉じるには、次のような組み込みの close()
関数を使用します。
close(ch)
チャネルを閉じると、そのチャネルでデータは再び送信されなくなります。 閉じたチャネルにデータを送信しようとすると、プログラムはパニックになります。 また、閉じたチャネルからデータを受信しようとすると、送信されたすべてのデータを読み取ることができます。 それ以降、"読み取り" を行うたびにゼロ値が返されます。
前に作成したプログラムに戻り、チャネルを使用してスリープ機能を削除します。 まず、次のように main
関数に文字列チャネルを作成しましょう。
ch := make(chan string)
そしてスリープの行の time.Sleep(3 * time.Second)
を削除しましょう。
これで、チャネルを使用して goroutine 間で通信できるようになりました。 checkAPI
関数で結果を出力するのではなく、コードをリファクターして、そのメッセージをチャネル経由で送信しましょう。 その関数からチャネルを使用するには、パラメーターとしてチャネルを追加する必要があります。 checkAPI
関数は次のようになります。
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
テキストの出力は目的ではなく、書式設定されたテキストをチャネルの中で送信するだけのため、fmt.Sprintf
関数を使用する必要があります。 また、チャネル変数の後に <-
演算子を使用してデータを送信していることに注目してください。
次に、チャネル変数を送信し、データを受信して出力するように main
関数を変更する必要があります。次に例を示します。
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
チャネルからデータを読み取りたいということを示すために、チャネルの前に <-
演算子を使用する方法に注目してください。
このプログラムを再実行すると、次のような出力が表示されます。
ERROR: https://api.somewhereintheinternet.com/ is down!
Done! It took 0.007401217 seconds!
少なくとも、スリープ関数を呼び出さなくても機能しているようです。 ただし、目的の動作ではありません。 goroutine の 1 つからの出力のみが表示されていますが、作成したのは 5 つです。 次のセクションで、このプログラムがこのように動作する理由を見てみましょう。
バッファーなしのチャネル
make()
関数を使用してチャネルを作成すると、バッファーなしのチャネルが作成されます。これが既定の動作です。 誰かがデータを受信する準備ができるまで、送信操作はバッファーなしのチャネルによってブロックされます。 前述のように、送信と受信はブロッキング操作です。 このブロッキング操作は、前のセクションのプログラムが最初のメッセージを受信するとすぐに停止する理由でもあります。
まず fmt.Print(<-ch)
によってプログラムがブロックされていると言えるのは、そのチャネルから読み取り、何かデータが到着するまで待機するという処理のためです。 何かが起こるとすぐに次の行に進み、プログラムは終了します。
残りの goroutine はどうなったでしょうか? それらはまだ実行されていますが、リッスンされていません。 また、プログラムは早期に終了したため、一部の goroutine からはデータを送信できませんでした。 この点を証明するために、次のようにもう 1 つの fmt.Print(<-ch)
を追加しましょう。
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
fmt.Print(<-ch)
このプログラムを再実行すると、次のような出力が表示されます。
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
Done! It took 0.263611711 seconds!
2 つの API の出力が表示されていることに注目してください。 fmt.Print(<-ch)
の行を追加し続けると、最終的に、チャネルに送信されているすべてのデータを読み取られます。 ただし、さらにデータを読み取ろうとしたときに、誰もデータを送信していない場合はどうなるでしょうか? たとえば、次のような場合です。
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
fmt.Print(<-ch)
このプログラムを再実行すると、次のような出力が表示されます。
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
SUCCESS: https://dev.azure.com is up and running!
これは動作していますが、プログラムは完了していません。 データの受信が想定されているため、最後の出力行により、ブロックされています。 Ctrl+C
のようなコマンドでプログラムを閉じる必要があります。
前の例は、データの読み取りとデータの受信はブロッキング操作であることを証明するだけです。 この問題を解決するには、次の例のように for
ループのコードを変更して、送信していることが確実なデータのみを受信します。
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
機能に問題があったバージョンの最終的なバージョンのプログラムは次のとおりです。
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
start := time.Now()
apis := []string{
"https://management.azure.com",
"https://dev.azure.com",
"https://api.github.com",
"https://outlook.office.com/",
"https://api.somewhereintheinternet.com/",
"https://graph.microsoft.com",
}
ch := make(chan string)
for _, api := range apis {
go checkAPI(api, ch)
}
for i := 0; i < len(apis); i++ {
fmt.Print(<-ch)
}
elapsed := time.Since(start)
fmt.Printf("Done! It took %v seconds!\n", elapsed.Seconds())
}
func checkAPI(api string, ch chan string) {
_, err := http.Get(api)
if err != nil {
ch <- fmt.Sprintf("ERROR: %s is down!\n", api)
return
}
ch <- fmt.Sprintf("SUCCESS: %s is up and running!\n", api)
}
このプログラムを再実行すると、次のような出力が表示されます。
ERROR: https://api.somewhereintheinternet.com/ is down!
SUCCESS: https://api.github.com is up and running!
SUCCESS: https://management.azure.com is up and running!
SUCCESS: https://dev.azure.com is up and running!
SUCCESS: https://graph.microsoft.com is up and running!
SUCCESS: https://outlook.office.com/ is up and running!
Done! It took 0.602099714 seconds!
このプログラムでは、目的の処理が実行されています。 スリープ機能を使用しなくなりました。チャネルを使用しています。 コンカレンシーを使用しない場合の約 2 秒ではなく、約 600 ミリ秒かかることにも注目してください。
最終的に、バッファーなしのチャネルにより、送信操作と受信操作が同期されていると言うことができます。 コンカレンシーを使用している場合でも、通信は "同期的" です。