通信メカニズムとしてチャネルを使用する

完了

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 ミリ秒かかることにも注目してください。

最終的に、バッファーなしのチャネルにより、送信操作と受信操作が同期されていると言うことができます。 コンカレンシーを使用している場合でも、通信は "同期的" です。