這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。歡迎來到 [Golang 系列教程](https://studygolang.com/subject/2)的第 22 篇。 在[上一教程](https://studygolang.com/articles/12342)裡,我們探討了如何使用 Go 協程(Goroutine)來實現並發。我們接著在本教程裡學習通道(Channel),學習如何通過通道來實現 Go 協程間的通訊。 ## 什麼是通道?通道可以想像成 Go 協程之間通訊的管道。如同管道中的水會從一端流到另一端,通過使用通道,資料也可以從一端發送,在另一端接收。 ## 通道的聲明所有通道都關聯了一個類型。通道只能運輸這種類型的資料,而運輸其他類型的資料都是非法的。 `chan T` 表示 `T` 類型的通道。 通道的零值為 `nil`。通道的零值沒有什麼用,應該像對 map 和切片所做的那樣,用 `make` 來定義通道。 下面編寫代碼,聲明一個通道。 ```gopackage mainimport "fmt"func main() { var a chan intif a == nil {fmt.Println("channel a is nil, going to define it")a = make(chan int)fmt.Printf("Type of a is %T", a)}}```[線上運行程式](https://play.golang.org/p/QDtf6mvymD) 由於通道的零值為 `nil`,在第 6 行,通道 `a` 的值就是 `nil`。於是,程式執行了 if 語句內的語句,定義了通道 `a`。程式中 `a` 是一個 int 類型的通道。該程式會輸出: ```channel a is nil, going to define it Type of a is chan int ```簡短聲明通常也是一種定義通道的簡潔有效方法。 ```goa := make(chan int) ```這一行代碼同樣定義了一個 int 類型的通道 `a`。 ## 通過通道進行發送和接收如下所示,該文法通過通道發送和接收資料。 ```godata := <- a // 讀取通道 a a <- data // 寫入通道 a ```通道旁的箭頭方向指定了是發送資料還是接收資料。 在第一行,箭頭對於 `a` 來說是向外指的,因此我們讀取了通道 `a` 的值,並把該值儲存到變數 `data`。 在第二行,箭頭指向了 `a`,因此我們在把資料寫入通道 `a`。 ## 發送與接收預設是阻塞的發送與接收預設是阻塞的。這是什麼意思?當把資料發送到通道時,程式控制會在發送資料的語句處發生阻塞,直到有其它 Go 協程從通道讀取到資料,才會解除阻塞。與此類似,當讀取通道的資料時,如果沒有其它的協程把資料寫入到這個通道,那麼讀取過程就會一直阻塞著。 通道的這種特效能夠協助 Go 協程之間進行高效的通訊,不需要用到其他程式設計語言常見的顯式鎖或條件變數。 ## 通道的程式碼範例理論已經夠了:)。接下來寫點代碼,看看協程之間通過通道是怎麼通訊的吧。 我們其實可以重寫上章學習 [Go 協程](https://studygolang.com/articles/12342) 時寫的程式,現在我們在這裡用上通道。 首先引用前面教程裡的程式。 ```gopackage mainimport ( "fmt""time")func hello() { fmt.Println("Hello world goroutine")}func main() { go hello()time.Sleep(1 * time.Second)fmt.Println("main function")}```[線上運行程式](https://play.golang.org/p/U9ZZuSql8-) 這是上一篇的代碼。我們使用到了休眠,使 Go 主協程等待 hello 協程結束。如果你看不懂,建議你閱讀上一教程 [Go 協程](https://studygolang.com/articles/12342)。 我們接下來使用通道來重寫上面代碼。 ```gopackage mainimport ( "fmt")func hello(done chan bool) { fmt.Println("Hello world goroutine")done <- true}func main() { done := make(chan bool)go hello(done)<-donefmt.Println("main function")}```[線上運行程式](https://play.golang.org/p/I8goKv6ZMF) 在上述程式裡,我們在第 12 行建立了一個 bool 類型的通道 `done`,並把 `done` 作為參數傳遞給了 `hello` 協程。在第 14 行,我們通過通道 `done` 接收資料。這一行代碼發生了阻塞,除非有協程向 `done` 寫入資料,否則程式不會跳到下一行代碼。於是,這就不需要用以前的 `time.Sleep` 來阻止 Go 主協程退出了。 `<-done` 這行代碼通過協程(譯註:原文筆誤,通道)`done` 接收資料,但並沒有使用資料或者把資料存放區到變數中。這完全是合法的。 現在我們的 Go 主協程發生了阻塞,等待通道 `done` 發送的資料。該通道作為參數傳遞給了協程 `hello`,`hello` 列印出 `Hello world goroutine`,接下來向 `done` 寫入資料。當完成寫入時,Go 主協程會通過通道 `done` 接收資料,於是它解除阻塞狀態,列印出文本 `main function`。 該程式輸出如下: ```Hello world goroutine main function ```我們稍微修改一下程式,在 `hello` 協程裡加入休眠函數,以便更好地理解阻塞的概念。 ```gopackage mainimport ( "fmt""time")func hello(done chan bool) { fmt.Println("hello go routine is going to sleep")time.Sleep(4 * time.Second)fmt.Println("hello go routine awake and going to write to done")done <- true}func main() { done := make(chan bool)fmt.Println("Main going to call hello go goroutine")go hello(done)<-donefmt.Println("Main received data")}```[線上運行程式](https://play.golang.org/p/EejiO-yjUQ) 在上面程式裡,我們向 `hello` 函數裡添加了 4 秒的休眠(第 10 行)。 程式首先會列印 `Main going to call hello go goroutine`。接著會開啟 `hello` 協程,列印 `hello go routine is going to sleep`。列印完之後,`hello` 協程會休眠 4 秒鐘,而在這期間,主協程會在 `<-done` 這一行發生阻塞,等待來自通道 `done` 的資料。4 秒鐘之後,列印 `hello go routine awake and going to write to done`,接著再列印 `Main received data`。 ## 通道的另一個樣本我們再編寫一個程式來更好地理解通道。該程式會計算一個數中每一位的平方和與立方和,然後把平方和與立方和相加並列印出來。例如,如果輸出是 123,該程式會如下計算輸出: ```squares = (1 * 1) + (2 * 2) + (3 * 3) cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) output = squares + cubes = 50```我們會這樣去構建程式:在一個單獨的 Go 協程計算平方和,而在另一個協程計算立方和,最後在 Go 主協程把平方和與立方和相加。 ```gopackage mainimport ( "fmt")func calcSquares(number int, squareop chan int) { sum := 0for number != 0 {digit := number % 10sum += digit * digitnumber /= 10}squareop <- sum}func calcCubes(number int, cubeop chan int) { sum := 0 for number != 0 {digit := number % 10sum += digit * digit * digitnumber /= 10}cubeop <- sum} func main() { number := 589sqrch := make(chan int)cubech := make(chan int)go calcSquares(number, sqrch)go calcCubes(number, cubech)squares, cubes := <-sqrch, <-cubechfmt.Println("Final output", squares + cubes)}```[線上運行程式](https://play.golang.org/p/4RKr7_YO_B) 在第 7 行,函數 `calcSquares` 計算一個數每位的平方和,並把結果發送給通道 `squareop`。與此類似,在第 17 行函數 `calcCubes` 計算一個數每位的立方和,並把結果發送給通道 `cubop`。 這兩個函數分別在單獨的協程裡運行(第 31 行和第 32 行),每個函數都有傳遞通道的參數,以便寫入資料。Go 主協程會在第 33 行等待兩個通道傳來的資料。一旦從兩個通道接收完資料,資料就會儲存在變數 `squares` 和 `cubes` 裡,然後計算並列印出最後結果。該程式會輸出: ```Final output 1536 ```## 死結使用通道需要考慮的一個重點是死結。當 Go 協程給一個通道發送資料時,照理說會有其他 Go 協程來接收資料。如果沒有的話,程式就會在運行時觸發 panic,形成死結。 同理,當有 Go 協程等著從一個通道接收資料時,我們期望其他的 Go 協程會向該通道寫入資料,要不然程式就會觸發 panic。 ```gopackage mainfunc main() { ch := make(chan int)ch <- 5}```[線上運行程式](https://play.golang.org/p/q1O5sNx4aW) 在上述程式中,我們建立了一個通道 `ch`,接著在下一行 `ch <- 5`,我們把 `5` 發送到這個通道。對於本程式,沒有其他的協程從 `ch` 接收資料。於是程式觸發 panic,出現如下執行階段錯誤。 ```fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]: main.main() /tmp/sandbox249677995/main.go:6 +0x80```## 單向通道我們目前討論的通道都是雙向通道,即通過通道既能發送資料,又能接收資料。其實也可以建立單向通道,這種通道只能發送或者接收資料。 ```gopackage mainimport "fmt"func sendData(sendch chan<- int) { sendch <- 10}func main() { sendch := make(chan<- int)go sendData(sendch)fmt.Println(<-sendch)}```[線上運行程式](https://play.golang.org/p/PRKHxM-iRK) 上面程式的第 10 行,我們建立了唯送(Send Only)通道 `sendch`。`chan<- int` 定義了唯送通道,因為箭頭指向了 `chan`。在第 12 行,我們試圖通過唯送通道接收資料,於是編譯器報錯: ```main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)```**一切都很順利,只不過一個不能讀取資料的唯送通道究竟有什麼意義呢?** **這就需要用到通道轉換(Channel Conversion)了。把一個雙向通道轉換成唯送通道或者唯收(Receive Only)通道都是行得通的,但是反過來就不行。** ```gopackage mainimport "fmt"func sendData(sendch chan<- int) { sendch <- 10}func main() { cha1 := make(chan int)go sendData(cha1)fmt.Println(<-cha1)}```[線上運行程式](https://play.golang.org/p/aqi_rJ1U8j) 在上述程式的第 10 行,我們建立了一個雙向通道 `cha1`。在第 11 行 `cha1` 作為參數傳遞給了 `sendData` 協程。在第 5 行,函數 `sendData` 裡的參數 `sendch chan<- int` 把 `cha1` 轉換為一個唯送通道。於是該通道在 `sendData` 協程裡是一個唯送通道,而在 Go 主協程裡是一個雙向通道。該程式最終列印輸出 `10`。 ## 關閉通道和使用 for range 遍曆通道資料發送方可以關閉通道,通知接收方這個通道不再有資料發送過來。 當從通道接收資料時,接收方可以多用一個變數來檢查通道是否已經關閉。 ```v, ok := <- ch ```上面的語句裡,如果成功接收通道所發送的資料,那麼 `ok` 等於 true。而如果 `ok` 等於 false,說明我們試圖讀取一個關閉的通道。從關閉的通道讀取到的值會是該通道類型的零值。例如,當通道是一個 `int` 類型的通道時,那麼從關閉的通道讀取的值將會是 `0`。 ```gopackage mainimport ( "fmt")func producer(chnl chan int) { for i := 0; i < 10; i++ {chnl <- i}close(chnl)}func main() { ch := make(chan int)go producer(ch)for {v, ok := <-chif ok == false {break}fmt.Println("Received ", v, ok)}}```[線上運行程式](https://play.golang.org/p/XWmUKDA2Ri) 在上述的程式中,`producer` 協程會從 0 到 9 寫入通道 `chn1`,然後關閉該通道。主函數有一個無限的 for 迴圈(第 16 行),使用變數 `ok`(第 18 行)檢查通道是否已經關閉。如果 `ok` 等於 false,說明通道已經關閉,於是退出 for 迴圈。如果 `ok` 等於 true,會列印出接收到的值和 `ok` 的值。 ```Received 0 true Received 1 true Received 2 true Received 3 true Received 4 true Received 5 true Received 6 true Received 7 true Received 8 true Received 9 true ```for range 迴圈用於在一個通道關閉之前,從通道接收資料。 接下來我們使用 for range 迴圈重寫上面的代碼。 ```gopackage mainimport ( "fmt")func producer(chnl chan int) { for i := 0; i < 10; i++ {chnl <- i}close(chnl)}func main() { ch := make(chan int)go producer(ch)for v := range ch {fmt.Println("Received ",v)}}```[線上運行程式](https://play.golang.org/p/JJ3Ida1r_6) 在第 16 行,for range 迴圈從通道 `ch` 接收資料,直到該通道關閉。一旦關閉了 `ch`,迴圈會自動結束。該程式會輸出: ```Received 0 Received 1 Received 2 Received 3 Received 4 Received 5 Received 6 Received 7 Received 8 Received 9 ```我們可以使用 for range 迴圈,重寫[通道的另一個樣本](#)這一節裡面的代碼,提高代碼的可重用性。 如果你仔細觀察這段代碼,會發現獲得一個數裡的每位元的代碼在 `calcSquares` 和 `calcCubes` 兩個函數內重複了。我們將把這段代碼抽離出來,放在一個單獨的函數裡,然後並發地調用它。 ```gopackage mainimport ( "fmt")func digits(number int, dchnl chan int) { for number != 0 {digit := number % 10dchnl <- digitnumber /= 10}close(dchnl)}func calcSquares(number int, squareop chan int) { sum := 0dch := make(chan int)go digits(number, dch)for digit := range dch {sum += digit * digit}squareop <- sum}func calcCubes(number int, cubeop chan int) { sum := 0dch := make(chan int)go digits(number, dch)for digit := range dch {sum += digit * digit * digit}cubeop <- sum}func main() { number := 589sqrch := make(chan int)cubech := make(chan int)go calcSquares(number, sqrch)go calcCubes(number, cubech)squares, cubes := <-sqrch, <-cubechfmt.Println("Final output", squares+cubes)}```[線上運行程式](https://play.golang.org/p/oL86W9Ui03) 上述程式裡的 `digits` 函數,包含了擷取一個數的每位元的邏輯,並且 `calcSquares` 和 `calcCubes` 兩個函數並發地調用了 `digits`。當計算完數字裡面的每一位元時,第 13 行就會關閉通道。`calcSquares` 和 `calcCubes` 兩個協程使用 for range 迴圈分別監聽了它們的通道,直到該通道關閉。程式的其他地方不變,該程式同樣會輸出: ```Final output 1536 ```本教程的內容到此結束。關於通道還有一些其他的概念,比如緩衝通道(Buffered Channel)、工作池(Worker Pool)和 select。我們會在接下來的教程裡專門介紹它們。感謝閱讀。祝你愉快。 **上一教程 - [Go 協程](https://studygolang.com/articles/12342)****下一教程 - [緩衝通道和工作池](https://studygolang.com/articles/12512)**
via: https://golangbot.com/channels/
作者:Nick Coghlan 譯者:Noluye 校對:polaris1119
本文由 GCTT 原創編譯,Go語言中文網 榮譽推出
本文由 GCTT 原創翻譯,Go語言中文網 首發。也想加入譯者行列,為開源做一些自己的貢獻嗎?歡迎加入 GCTT!
翻譯工作和譯文發表僅用於學習和交流目的,翻譯工作遵照 CC-BY-NC-SA 協議規定,如果我們的工作有侵犯到您的權益,請及時聯絡我們。
歡迎遵照 CC-BY-NC-SA 協議規定 轉載,敬請在本文中標註並保留原文/譯文連結和作者/譯者等資訊。
文章僅代表作者的知識和看法,如有不同觀點,請樓下排隊吐槽
4062 次點擊