- 原文地址:Part 22: Channels
- 原文作者:Naveen R
- 譯者:咔嘰咔嘰 轉載請註明出處。
在上一個教程中,我們討論了 Go 中如何使用Goroutines
實現併發。在本教程中,我們將討論有關channel
以及Goroutines
如何使用channel
進行通訊。
什麼是channel
channel
可以被認為是Goroutines
通訊的管道。類似於水在管道中從一端流到另一端的方式,資料可以從一端傳送,可以從另一端接收。
channel
的宣告
每個channel
都有一個與之關聯的型別。此型別是允許channel
傳輸的資料型別。不允許使用該channel
傳輸其他型別。
chan T
代表型別為T
的channel
channel
的零值為nil
。nil channel
沒有任何用處,因此得使用類似於make map
和 make slice
來定義它。
讓我們寫一些宣告channel
的程式碼。
package main
import "fmt"
func main() {
var a chan int
if a == nil {
fmt.Println("channel a is nil, going to define it")
a = make(chan int)
fmt.Printf("Type of a is %T", a)
}
}
複製程式碼
在第 6 行宣告瞭var a chan int
,可以看到channel
的零值為nil
。因此,執行if
條件內的語句並定義channel
。上面的程式中的a
是一個int channel
。該程式將輸出,
channel a is nil, going to define it
Type of a is chan int
複製程式碼
使用make
宣告也是定義channel
的有效而簡潔的方法。
a := make(chan int)
複製程式碼
上面的程式碼行定義了一個int
型的channel a
channel
的傳送和接收
下面給出了從channel
傳送和接收資料的語法,
data := <- a // read from channel a
a <- data // write to channel a
複製程式碼
箭頭相對於通道的方向指定了是傳送還是接收資料。
在第 1 行中,箭頭從a
向指向data
,因此我們從通道a
讀取並將值儲存到變數data
中。
在第 2 行中,箭頭指向a
,因此我們把data
寫入通道a
。
傳送和接收預設是阻塞的
預設情況下,傳送和接收是阻塞的。這是什麼意思?當資料傳送到channel
時,傳送方被阻塞直到其他Goroutine
從該channel
讀取出資料。類似地,當從channel
讀取資料時,讀取方被阻塞,直到其他Goroutine
將資料寫入該channel
。
channel
的這種屬性有助於Goroutines
有效地進行通訊,而無需使用在其他程式語言中常見的顯式鎖或條件變數。
channel
示例程式碼
讓我們編寫一個程式來了解Goroutines
如何使用channel
進行通訊。
我們在上一篇教程中引用過這個程式。
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello world goroutine")
}
func main() {
go hello()
time.Sleep(1 * time.Second)
fmt.Println("main function")
}
複製程式碼
Run in playgroud
這是上一個教程的程式碼,這裡我們將使用channel
重寫上述程式。
package main
import (
"fmt"
)
func hello(done chan bool) {
fmt.Println("Hello world goroutine")
done <- true
}
func main() {
done := make(chan bool)
go hello(done)
<-done
fmt.Println("main function")
}
複製程式碼
在上面的程式中,我們在第一行建立了一個bool
型的done channel
。 並將其作為引數傳遞給hello
。第 14 行我們正在從done channel
接收資料。這行程式碼是阻塞的,這意味著在Goroutine
將資料寫入done channel
之前將會一直阻塞。因此,上一個程式中的time.Sleep
的就沒有必要了,用sleep
對程式而言是相當不友好。
程式碼行<-done
表示從done channel
接收資料,如果沒有任何變數使用或儲存該資料,這是完全合法的。
現在我們的main Goroutine
被阻塞直到done channel
有資料寫入。 hello Goroutine
接收done channel
作為引數,列印Hello world goroutine
然後把true
寫入done channel
。當這個寫入完成時,main Goroutine
從該done channel
接收資料,然後結束阻塞列印了main
函式的文字。
輸出,
Hello world goroutine
main function
複製程式碼
讓我們通過在hello Goroutine
中引入一個sleep
來修改這個程式,以更好地理解這個阻塞概念。
package main
import (
"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)
<-done
fmt.Println("Main received data")
}
複製程式碼
這個程式將首先列印Main going to call hello go goroutine
。然後hello Goroutine
啟動,列印hello go routine is going to sleep
。列印完成後,hello Goroutine
將休眠 4 秒鐘,在此期間main Goroutine
將被阻塞,因為它正在等待來自<-done
的通道的資料。 4 秒後hello Goroutine
甦醒,然後列印hello go routine awake and going to write to done
並寫入資料到channel
,接著main Goroutine
接收資料並列印Main received data
。
channel 的另外一個例子
讓我們再寫一個程式來更好地理解,該程式將列印數字各個位的平方和立方的總和。
例如,如果 123 是輸入,則此程式將計算輸出為
squares = (1 * 1) + (2 * 2) + (3 * 3) cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3) output = squares + cubes = 50
我們將構建該程式,使得平方在一個Goroutine
中計算,而立方在另一個Goroutine
中進行計算,最終在main Goroutine
中求和。
package main
import (
"fmt"
)
func calcSquares(number int, squareop chan int) {
sum := 0
for number != 0 {
digit := number % 10
sum += digit * digit
number /= 10
}
squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
sum := 0
for number != 0 {
digit := number % 10
sum += digit * digit * digit
number /= 10
}
cubeop <- sum
}
func main() {
number := 589
sqrch := make(chan int)
cubech := make(chan int)
go calcSquares(number, sqrch)
go calcCubes(number, cubech)
squares, cubes := <-sqrch, <-cubech
fmt.Println("Final output", squares + cubes)
}
複製程式碼
calcSquares
函式計算各個數字的平方的和,並將其傳送到squares channel
。類似地,calcCubes
計算各個數字的立方的和並將其傳送到cubes channel
。
這兩個函式都作為單獨的Goroutines
執行。每個函式都通過一個channel
作為入參。main Goroutine
等待來自這兩個channel
的資料。一旦從兩個channel
接收到資料,它們就儲存在squares
和cubes
中求和,然後列印最終輸出。該程式將列印,
Final output 1536
複製程式碼
死鎖
使用channel
時要考慮的一個重要因素是死鎖。如果Goroutine
正在channel
上傳送資料,那麼期待其他一些Goroutine
接收資料。如果傳送的資料沒有被消費,程式將在執行時產生一個panic
。
同樣,如果Goroutine
正在等待從一個channel
接收資料,那麼其他Goroutine
應該在該channel
上寫入資料,否則程式也會出現panic
。
package main
func main() {
ch := make(chan int)
ch <- 5
}
複製程式碼
在上面的程式中,建立了一個channel ch
,我們用ch <-5
向channel
傳送 5。在該程式中,沒有其他Goroutine
從ch
接收資料。因此,此程式將出現以下執行時錯誤。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox249677995/main.go:6 +0x80
複製程式碼
單向channel
到目前為止我們討論的所有channel
都是雙向channel
,即資料可以在它們上傳送和接收。也可以建立單向channel
,即僅傳送或接收資料的channel
。
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
sendch := make(chan<- int)
go sendData(sendch)
fmt.Println(<-sendch)
}
複製程式碼
在上面的程式中,我們在第 10 行中建立了僅傳送channel sendch
。chan < - int
表示當箭頭指向chan
時僅為傳送channel
。我們在第 12 行中嘗試從該channel
接收資料。 發現這是不允許的,當程式編譯時,編譯器會報錯,
main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)
複製程式碼
看起來好像沒啥問題,但是一個寫channel
僅僅用來寫,而不能用來讀這樣有啥意義!
我們接下來將用到channel
轉化。可以將雙向channel
轉換為僅傳送或僅接收的channel
,反之亦然。
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
chnl := make(chan int)
go sendData(chnl)
fmt.Println(<-chnl)
}
複製程式碼
在上面的程式第 10 行,建立了雙向channel chnl
。在第 11 行,它作為引數傳遞給sendData Goroutine
,而sendData
函式在第 5 行用sendch chan < - int
將此chnl
轉換為僅傳送的channel
型別。所以現在通道只在sendData Goroutine
中是單向的,但它在main Goroutine
中是雙向的。該程式將列印 10 作為輸出。(譯者注:這就是單向channel
的用途,定義函式或者方法的時候,使用只讀或只寫會讓程式碼更健壯。)
關閉channel
和迴圈channel
傳送者能夠關閉channel
以通知接收者不再在該channel
上傳送資料。
接收者可以在從channel
接收資料時使用額外的變數來檢查channel
是否已關閉。
v, ok := <- ch
複製程式碼
在上面的語句中,如果成功地從該操作中接收到該值,則ok
為true
。如果ok
為false
,則表示我們正在從一個關閉的channel
中讀取。從關閉的channel
中讀取的值將是通道型別的零值。例如,如果是int
型別,則從關閉的channel
中讀取到的值將為 0。
package main
import (
"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 := <-ch
if ok == false {
break
}
fmt.Println("Received ", v, ok)
}
}
複製程式碼
在上面的程式中,生產者Goroutine
將 0 到 9 寫入channel chnl
,然後關閉它。在第 16 行main
函式有一個無限for
迴圈,它使變數ok
檢查channel
是否被關閉。如果ok
為false
,則表示已關閉,因此迴圈中斷。否則,將列印收到的值和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 迴圈的for range
形式可用於從channel
接收值,直到它被關閉。
讓我們使用for range
迴圈重寫上面的程式。
package main
import (
"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)
}
}
複製程式碼
for range
迴圈在第 16 行接收來自channel ch
的資料直到它被關閉。 ch
關閉後,迴圈自動退出。該程式輸出,
Received 0
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Received 8
Received 9
複製程式碼
我們來重寫一下上面那個求平方立方和的程式,
如果仔細檢視程式,可以注意到在calcSquares
函式和calcCubes
函式中獲取每一位的數字的邏輯重複了。我們將該邏輯的程式碼抽出來,然後分別在那兩個函式中併發呼叫這個函式。
package main
import (
"fmt"
)
func digits(number int, dchnl chan int) {
for number != 0 {
digit := number % 10
dchnl <- digit
number /= 10
}
close(dchnl)
}
func calcSquares(number int, squareop chan int) {
sum := 0
dch := make(chan int)
go digits(number, dch)
for digit := range dch {
sum += digit * digit
}
squareop <- sum
}
func calcCubes(number int, cubeop chan int) {
sum := 0
dch := make(chan int)
go digits(number, dch)
for digit := range dch {
sum += digit * digit * digit
}
cubeop <- sum
}
func main() {
number := 589
sqrch := make(chan int)
cubech := make(chan int)
go calcSquares(number, sqrch)
go calcCubes(number, cubech)
squares, cubes := <-sqrch, <-cubech
fmt.Println("Final output", squares+cubes)
}
複製程式碼
上面程式中的digits
函式現在包含從number
中獲取各位的邏輯,並且它同時由calcSquares
和calcCubes
函式呼叫。一旦number
中沒有更多的位,channel
就會在第 13 行被關閉。 calcSquares
和calcCubes Goroutines
使用for range
迴圈監聽各自的channel
,直到它關閉。該程式的其餘部分和之前的例子是相同的。該程式也會列印
Final output 1536
複製程式碼
該節教程就結束了,channel
中還有更多的概念,例如緩衝channel
,worker pool
和select
。我們將在下一個教程中討論它們。