22. 信道(channel)
什么是信道蚕甥?
信道可以想像成 Go 協(xié)程之間通信的管道。如同管道中的水會從一端流到另一端柱彻,通過使用信道,數(shù)據(jù)也可以從一端發(fā)送餐胀,在另一端接收哟楷。
信道的聲明
所有信道都關(guān)聯(lián)了一個類型。信道只能運輸這種類型的數(shù)據(jù)否灾,而運輸其他類型的數(shù)據(jù)都是非法的卖擅。
chan T
表示 T
類型的信道。
信道的零值為 nil
墨技。信道的零值沒有什么用惩阶,應(yīng)該像對 map 和切片所做的那樣,用 make
來定義信道扣汪。
下面編寫代碼断楷,聲明一個信道。
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)
}
}
由于信道的零值為 nil
私痹,在第 6 行脐嫂,信道 a
的值就是 nil
。于是紊遵,程序執(zhí)行了 if 語句內(nèi)的語句账千,定義了信道 a
。程序中 a
是一個 int 類型的信道暗膜。該程序會輸出:
channel a is nil, going to define it
Type of a is chan int
簡短聲明通常也是一種定義信道的簡潔有效的方法匀奏。
a := make(chan int)
這一行代碼同樣定義了一個 int 類型的信道 a
。
通過信道進(jìn)行發(fā)送和接收
如下所示学搜,該語法通過信道發(fā)送和接收數(shù)據(jù)娃善。
data := <- a // 讀取信道 a
a <- data // 寫入信道 a
信道旁的箭頭方向指定了是發(fā)送數(shù)據(jù)還是接收數(shù)據(jù)。
在第一行瑞佩,箭頭對于 a
來說是向外指的聚磺,因此我們讀取了信道 a
的值,并把該值存儲到變量 data
炬丸。
在第二行瘫寝,箭頭指向了 a
蜒蕾,因此我們在把數(shù)據(jù)寫入信道 a
。
發(fā)送與接收默認(rèn)是阻塞的
發(fā)送與接收默認(rèn)是阻塞的焕阿。這是什么意思咪啡?當(dāng)把數(shù)據(jù)發(fā)送到信道時,程序控制會在發(fā)送數(shù)據(jù)的語句處發(fā)生阻塞暮屡,直到有其它 Go 協(xié)程從信道讀取到數(shù)據(jù)撤摸,才會解除阻塞。與此類似褒纲,當(dāng)讀取信道的數(shù)據(jù)時准夷,如果沒有其它的協(xié)程把數(shù)據(jù)寫入到這個信道,那么讀取過程就會一直阻塞著外厂。
信道的這種特性能夠幫助 Go 協(xié)程之間進(jìn)行高效的通信冕象,不需要用到其他編程語言常見的顯式鎖或條件變量代承。
信道的代碼示例
理論已經(jīng)夠了:)汁蝶。接下來寫點代碼,看看協(xié)程之間通過信道是怎么通信的吧论悴。
我們其實可以重寫上章學(xué)習(xí) [Go 協(xié)程]時寫的程序掖棉,現(xiàn)在我們在這里用上信道。
首先引用前面教程里的程序膀估。
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")
}
這是上一篇的代碼幔亥。我們使用到了休眠,使 Go 主協(xié)程等待 hello 協(xié)程結(jié)束察纯。如果你看不懂帕棉,建議你閱讀上一教程 [Go 協(xié)程]。
我們接下來使用信道來重寫上面代碼饼记。
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")
}
在上述程序里香伴,我們在第 12 行創(chuàng)建了一個 bool 類型的信道 done
,并把 done
作為參數(shù)傳遞給了 hello
協(xié)程具则。在第 14 行即纲,我們通過信道 done
接收數(shù)據(jù)。這一行代碼發(fā)生了阻塞博肋,除非有協(xié)程向 done
寫入數(shù)據(jù)低斋,否則程序不會跳到下一行代碼。于是匪凡,這就不需要用以前的 time.Sleep
來阻止 Go 主協(xié)程退出了膊畴。
<-done
這行代碼通過協(xié)程(譯注:原文筆誤,信道)done
接收數(shù)據(jù)病游,但并沒有使用數(shù)據(jù)或者把數(shù)據(jù)存儲到變量中唇跨。這完全是合法的。
現(xiàn)在我們的 Go 主協(xié)程發(fā)生了阻塞,等待信道 done
發(fā)送的數(shù)據(jù)轻绞。該信道作為參數(shù)傳遞給了協(xié)程 hello
采记,hello
打印出 Hello world goroutine
,接下來向 done
寫入數(shù)據(jù)政勃。當(dāng)完成寫入時唧龄,Go 主協(xié)程會通過信道 done
接收數(shù)據(jù),于是它解除阻塞狀態(tài)奸远,打印出文本 main function
既棺。
該程序輸出如下:
Hello world goroutine
main function
我們稍微修改一下程序,在 hello
協(xié)程里加入休眠函數(shù)懒叛,以便更好地理解阻塞的概念丸冕。
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")
}
在上面程序里,我們向 hello
函數(shù)里添加了 4 秒的休眠(第 10 行)薛窥。
程序首先會打印 Main going to call hello go goroutine
胖烛。接著會開啟 hello
協(xié)程,打印 hello go routine is going to sleep
诅迷。打印完之后佩番,hello
協(xié)程會休眠 4 秒鐘,而在這期間罢杉,主協(xié)程會在 <-done
這一行發(fā)生阻塞趟畏,等待來自信道 done
的數(shù)據(jù)。4 秒鐘之后滩租,打印 hello go routine awake and going to write to done
赋秀,接著再打印 Main received data
。
信道的另一個示例
我們再編寫一個程序來更好地理解信道律想。該程序會計算一個數(shù)中每一位的平方和與立方和猎莲,然后把平方和與立方和相加并打印出來。
例如蜘欲,如果輸出是 123益眉,該程序會如下計算輸出:
squares = (1 * 1) + (2 * 2) + (3 * 3)
cubes = (1 * 1 * 1) + (2 * 2 * 2) + (3 * 3 * 3)
output = squares + cubes = 50
我們會這樣去構(gòu)建程序:在一個單獨的 Go 協(xié)程計算平方和,而在另一個協(xié)程計算立方和姥份,最后在 Go 主協(xié)程把平方和與立方和相加郭脂。
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)
}
在第 7 行,函數(shù) calcSquares
計算一個數(shù)每位的平方和澈歉,并把結(jié)果發(fā)送給信道 squareop
展鸡。與此類似,在第 17 行函數(shù) calcCubes
計算一個數(shù)每位的立方和埃难,并把結(jié)果發(fā)送給信道 cubop
莹弊。
這兩個函數(shù)分別在單獨的協(xié)程里運行(第 31 行和第 32 行)涤久,每個函數(shù)都有傳遞信道的參數(shù),以便寫入數(shù)據(jù)忍弛。Go 主協(xié)程會在第 33 行等待兩個信道傳來的數(shù)據(jù)响迂。一旦從兩個信道接收完數(shù)據(jù),數(shù)據(jù)就會存儲在變量 squares
和 cubes
里细疚,然后計算并打印出最后結(jié)果蔗彤。該程序會輸出:
Final output 1536
死鎖
使用信道需要考慮的一個重點是死鎖。當(dāng) Go 協(xié)程給一個信道發(fā)送數(shù)據(jù)時疯兼,照理說會有其他 Go 協(xié)程來接收數(shù)據(jù)然遏。如果沒有的話,程序就會在運行時觸發(fā) panic吧彪,形成死鎖待侵。
同理,當(dāng)有 Go 協(xié)程等著從一個信道接收數(shù)據(jù)時姨裸,我們期望其他的 Go 協(xié)程會向該信道寫入數(shù)據(jù)秧倾,要不然程序就會觸發(fā) panic。
package main
func main() {
ch := make(chan int)
ch <- 5
}
在上述程序中啦扬,我們創(chuàng)建了一個信道 ch
中狂,接著在下一行 ch <- 5
,我們把 5
發(fā)送到這個信道扑毡。對于本程序,沒有其他的協(xié)程從 ch
接收數(shù)據(jù)盛险。于是程序觸發(fā) panic瞄摊,出現(xiàn)如下運行時錯誤。
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/tmp/sandbox249677995/main.go:6 +0x80
單向信道
我們目前討論的信道都是雙向信道苦掘,即通過信道既能發(fā)送數(shù)據(jù)换帜,又能接收數(shù)據(jù)。其實也可以創(chuàng)建單向信道鹤啡,這種信道只能發(fā)送或者接收數(shù)據(jù)惯驼。
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
sendch := make(chan<- int)
go sendData(sendch)
fmt.Println(<-sendch)
}
上面程序的第 10 行,我們創(chuàng)建了唯送(Send Only)信道 sendch
递瑰。chan<- int
定義了唯送信道祟牲,因為箭頭指向了 chan
。在第 12 行抖部,我們試圖通過唯送信道接收數(shù)據(jù)说贝,于是編譯器報錯:
main.go:11: invalid operation: <-sendch (receive from send-only type chan<- int)
一切都很順利,只不過一個不能讀取數(shù)據(jù)的唯送信道究竟有什么意義呢慎颗?
這就需要用到信道轉(zhuǎn)換(Channel Conversion)了乡恕。把一個雙向信道轉(zhuǎn)換成唯送信道或者唯收(Receive Only)信道都是行得通的言询,但是反過來就不行。
package main
import "fmt"
func sendData(sendch chan<- int) {
sendch <- 10
}
func main() {
cha1 := make(chan int)
go sendData(cha1)
fmt.Println(<-cha1)
}
在上述程序的第 10 行傲宜,我們創(chuàng)建了一個雙向信道 cha1
运杭。在第 11 行 cha1
作為參數(shù)傳遞給了 sendData
協(xié)程。在第 5 行函卒,函數(shù) sendData
里的參數(shù) sendch chan<- int
把 cha1
轉(zhuǎn)換為一個唯送信道县习。于是該信道在 sendData
協(xié)程里是一個唯送信道,而在 Go 主協(xié)程里是一個雙向信道谆趾。該程序最終打印輸出 10
躁愿。
關(guān)閉信道和使用 for range 遍歷信道
數(shù)據(jù)發(fā)送方可以關(guān)閉信道,通知接收方這個信道不再有數(shù)據(jù)發(fā)送過來沪蓬。
當(dāng)從信道接收數(shù)據(jù)時彤钟,接收方可以多用一個變量來檢查信道是否已經(jīng)關(guān)閉。
v, ok := <- ch
上面的語句里跷叉,如果成功接收信道所發(fā)送的數(shù)據(jù)逸雹,那么 ok
等于 true。而如果 ok
等于 false云挟,說明我們試圖讀取一個關(guān)閉的通道梆砸。從關(guān)閉的信道讀取到的值會是該信道類型的零值。例如园欣,當(dāng)信道是一個 int
類型的信道時帖世,那么從關(guān)閉的信道讀取的值將會是 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)
}
}
在上述的程序中沸枯,producer
協(xié)程會從 0 到 9 寫入信道 chn1
日矫,然后關(guān)閉該信道。主函數(shù)有一個無限的 for 循環(huán)(第 16 行)绑榴,使用變量 ok
(第 18 行)檢查信道是否已經(jīng)關(guān)閉哪轿。如果 ok
等于 false,說明信道已經(jīng)關(guān)閉翔怎,于是退出 for 循環(huán)窃诉。如果 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 循環(huán)用于在一個信道關(guān)閉之前飘痛,從信道接收數(shù)據(jù)。
接下來我們使用 for range 循環(huán)重寫上面的代碼于毙。
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)
}
}
在第 16 行敦冬,for range 循環(huán)從信道 ch
接收數(shù)據(jù),直到該信道關(guān)閉唯沮。一旦關(guān)閉了 ch
脖旱,循環(huán)會自動結(jié)束堪遂。該程序會輸出:
Received 0
Received 1
Received 2
Received 3
Received 4
Received 5
Received 6
Received 7
Received 8
Received 9
我們可以使用 for range 循環(huán),重寫[信道的另一個示例]這一節(jié)里面的代碼萌庆,提高代碼的可重用性溶褪。
如果你仔細(xì)觀察這段代碼,會發(fā)現(xiàn)獲得一個數(shù)里的每位數(shù)的代碼在 calcSquares
和 calcCubes
兩個函數(shù)內(nèi)重復(fù)了践险。我們將把這段代碼抽離出來猿妈,放在一個單獨的函數(shù)里,然后并發(fā)地調(diào)用它巍虫。
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
函數(shù)彭则,包含了獲取一個數(shù)的每位數(shù)的邏輯,并且 calcSquares
和 calcCubes
兩個函數(shù)并發(fā)地調(diào)用了 digits
占遥。當(dāng)計算完數(shù)字里面的每一位數(shù)時俯抖,第 13 行就會關(guān)閉信道。calcSquares
和 calcCubes
兩個協(xié)程使用 for range 循環(huán)分別監(jiān)聽了它們的信道瓦胎,直到該信道關(guān)閉芬萍。程序的其他地方不變,該程序同樣會輸出:
Final output 1536
關(guān)于信道還有一些其他的概念搔啊,比如緩沖信道(Buffered Channel)柬祠、工作池(Worker Pool)和 select。我們會在接下來的教程里專門介紹它們