使用通道作為通訊機制

已完成

在 Go 中,通道是 Goroutine 之間的通訊機制。 請記住,我們之前說過 Go 的並行處理方法是:「請不要藉由共用記憶體進行通訊;相反地,藉由通訊來共用記憶體」。需要將值從一個 Goroutine 傳送到另一個 Goroutine 時,您可以使用通道。 現在讓我們一起了解通道如何運作,以及如何使用通道來撰寫並行的 Go 程式。

通道的語法

因為通道是收送資料的通訊機制,所以也有類型的分別。 這表示您只能傳送資料給通道支援的類型。 您可以使用關鍵字 chan 做為通道的資料類型,但您也必須指定要通過通道的資料類型,例如 int 類型。

每當您在函式中宣告通道,或在參數中指定通道時,都必須使用 chan <type>,例如 chan int。 若要建立通道,您需要使用內建的 make() 函式:

ch := make(chan int)

通道可以執行傳送資料及接收資料兩項作業。 若要指定通道支援的作業類型,必須使用通道運算子 <-。 此外,在通道中執行傳送資料及接收資料兩項作業會受到限制。 後文中將會說明其原因。

當您想要表示通道只能傳送資料時,請在通道之後使用 <- 運算子。 當您想要表示通道接收資料時,請在通道之前使用 <- 運算子,如這些範例所示:

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

另一項可以在通道中使用的作業,就是關閉通道。 若要關閉通道,請使用內建的 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!

即便我們沒有呼叫睡眠函式,程式也照常運作了對吧? 但我們並未達成我們的目的。 我們建立了 5 個 Goroutine,卻只有一個產生輸出。 在下節中,我們將會探討程式為何如此運作。

無法緩衝的通道

根據預設,當您使用 make() 函式建立通道時,也會建立一條無法緩衝的通道。 無法緩衝的通道會等待有人可以接收資料時,才會執行傳送作業。 如先前所述,在通道中執行傳送資料及接收資料兩項作業會受到限制的原因。 此封鎖作業也是上節中,程式在收到第一則訊息後隨即停止的原因。

我們可以從 fmt.Print(<-ch) 限制程式執行開始說起,因為程式會從通道讀取資料,並等候資料到達。 當程式取得資料之後,就會繼續執行下一行,直到程式完成。

那麼剩下的 Goroutine 又會如何? 這些 Goroutine 仍會繼續執行,但其中無一執行接聽的工作。 加上程式提早完成,導致有一些 Goroutine 無法傳送資料。 我們可以新增另一個 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!

請注意,您現在會看到兩個 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!

程式會執行應執行的動作。 您不再使用睡眠函式,而會使用通道。 另外也請注意,若未使用並行,現在大約 600 毫秒就能完成,而不需要將近 2 秒。

最後,我們可以說無法緩衝的通道會同步傳送及接收作業。 即使使用並行,通訊仍會同步