了解可緩衝的通道
如您前面所學,通道根據預設是無法緩衝。 這表示僅當有接收作業時,才會接受傳送作業。 否則將會限制程式一直等待。
有時候,您會需要在 Goroutine 之間進行這種類型的同步。 不過,有時候您可能只想實作並行,不需要限制 Goroutine 彼此相互通訊的方式。
因為可緩衝的通道與佇列的運作方式相同,所以不會限制程式傳送及接收資料。 當您建立通道時,可以限制此佇列的大小,如下所示:
ch := make(chan string, 10)
每當有項目傳送至通道時,就會新增至佇列。 然後,接收作業會從佇列中移除該項目。 當通道滿載時,所有傳送作業都會等候,直到有空間可以保存資料為止。 相反地,若通道是空的,而且有讀取作業,將會限制通道,直到讀取開始為止。
以下提供一簡單範例,協助您了解可緩衝的通道:
package main
import (
"fmt"
)
func send(ch chan string, message string) {
ch <- message
}
func main() {
size := 4
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
send(ch, "three")
send(ch, "four")
fmt.Println("All data sent to the channel ...")
for i := 0; i < size; i++ {
fmt.Println(<-ch)
}
fmt.Println("Done!")
}
當您執行程式時,會看到下列輸出:
All data sent to the channel ...
one
two
three
four
Done!
您可能會覺得,我們在這裡沒有做什麼不同的事,您想得沒錯。 但是,讓我們看看,當您將 size
變數變更為較小的數字時 (您也可以試試較大的數字) 會如何,如下所示:
size := 2
當您重新執行程式時,會收到下列錯誤:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.send(...)
/Users/developer/go/src/concurrency/main.go:8
main.main()
/Users/developer/go/src/concurrency/main.go:16 +0xf3
exit status 2
原因是對 send
函式的呼叫是連續的。 您並沒有建立新的 Goroutine。 因此不會有佇列。
通道與 Goroutine 緊密相連。 若無其他 Goroutine 接收來自通道的資料,整個程式可能會永遠處於受限狀態。 如您所見,這確實會發生。
現在讓我們做件有趣的事! 我們為最後兩個呼叫建立 Goroutine (先前兩個呼叫已正確排入緩衝),然後執行 for 迴圈四次。 程式碼如下:
func main() {
size := 2
ch := make(chan string, size)
send(ch, "one")
send(ch, "two")
go send(ch, "three")
go send(ch, "four")
fmt.Println("All data sent to the channel ...")
for i := 0; i < 4; i++ {
fmt.Println(<-ch)
}
fmt.Println("Done!")
}
當您執行此程式時,程式會如預期般運作。 當您使用通道時,建議您一律使用 Goroutine。
讓我們測試您建立具有超過所需元素的緩衝通道案例。 我們將使用之前使用的範例來檢查 API 並建立一個大小為 10 的緩衝區通道:
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, 10)
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)
}
當您執行程式時,得到的輸出將會與先前相同。 您可以使用較小或較大的數字來變更通道大小,但程式仍可運作。
無法緩衝的通道與可緩衝的通道
至此,您可能很想知道,兩種通道的使用時機。 這完全取決於您希望 Goroutine 之間通訊的流動方式。 無法緩衝的通道會以同步方式通訊, 以確保每次傳送資料時,程式都會受到限制,直到有人從通道讀取資料為止。
反之,可緩衝的通道會分隔傳送與接收作業, 而不會限制程式,但請務必小心,這可能會造成死結 (如您先前所見)。 當您使用無法緩衝的通道時,可以控制可並行的 Goroutine 數量。 例如,您可能會想要呼叫 API,同時控制每秒執行的呼叫數。 否則,您可能會受到限制。
通道方向
Go 中的頻道有另一個有趣的功能。 當您在函式的參數中設定通道時,可以指定通道傳送或接收資料。 隨著程式愈來愈大,函式量可能會太多,建議記錄每個通道的意圖,以適當地使用。 若您正在撰寫程式庫,想要公開唯讀的通道,以維持資料一致性。
若要定義通道的方向,可以使用類似於讀取或接收資料的方式。 當您在函式參數中宣告通道時,就可以這麼做。 在函式的參數中定義通道類型的語法:
chan<- int // it's a channel to only send data
<-chan int // it's a channel to only receive data
當您透過通道傳送資料,而該通道只能接收時,您會在編譯程式時收到錯誤。
我們將在下列範例程式中使用兩個函式,各用於讀取資料及傳送資料:
package main
import "fmt"
func send(ch chan<- string, message string) {
fmt.Printf("Sending: %#v\n", message)
ch <- message
}
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
}
func main() {
ch := make(chan string, 1)
send(ch, "Hello World!")
read(ch)
}
當您執行程式時,會看到下列輸出:
Sending: "Hello World!"
Receiving: "Hello World!"
此程式會釐清每個函式中各個通道的意圖。 若您嘗試使用「只能接收」的通道來傳送資料,將會收到編譯錯誤。 例如,您可以嘗試執行下列動作:
func read(ch <-chan string) {
fmt.Printf("Receiving: %#v\n", <-ch)
ch <- "Bye!"
}
當您執行程式時,會看到下列錯誤:
# command-line-arguments
./main.go:12:5: invalid operation: ch <- "Bye!" (send to receive-only type <-chan string)
比起誤用通道,收到編譯錯誤是比較理想的情況。
多工
最後,我們來看看如何使用 select
關鍵字,同時與多個通道互動。 有時候,當您使用多個通道時,您會想要等候事件發生。 比方說,您可能會加入取消作業的邏輯,在程式處理的資料發生異常狀況時使用。
select
陳述式的運作方式就像是 switch
陳述式,但會用於通道。 該陳述式會限制程式執行,直到收到要處理的事件為止。 若陳述式收到多個事件,將會隨機選擇一個。
select
陳述式的特性是會在處理完事件之後結束執行。 若您想要等候更多事件發生,可能需要使用迴圈。
我們使用下列程式看看 select
的實際運作情況:
package main
import (
"fmt"
"time"
)
func process(ch chan string) {
time.Sleep(3 * time.Second)
ch <- "Done processing!"
}
func replicate(ch chan string) {
time.Sleep(1 * time.Second)
ch <- "Done replicating!"
}
func main() {
ch1 := make(chan string)
ch2 := make(chan string)
go process(ch1)
go replicate(ch2)
for i := 0; i < 2; i++ {
select {
case process := <-ch1:
fmt.Println(process)
case replicate := <-ch2:
fmt.Println(replicate)
}
}
}
當您執行程式時,會看到下列輸出:
Done replicating!
Done processing!
請注意,replicate
函式會先完成,這就是為什麼您會先在終端機中看到其輸出。 main 函式具有迴圈,因為 select
陳述式在收到事件時就會結束,但我們仍在等待 process
函式完成。