22. 信道(channel)

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ù)就會存儲在變量 squarescubes 里细疚,然后計算并打印出最后結(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<- intcha1 轉(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ù)的代碼在 calcSquarescalcCubes 兩個函數(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ù)的邏輯,并且 calcSquarescalcCubes 兩個函數(shù)并發(fā)地調(diào)用了 digits占遥。當(dāng)計算完數(shù)字里面的每一位數(shù)時俯抖,第 13 行就會關(guān)閉信道。calcSquarescalcCubes 兩個協(xié)程使用 for range 循環(huán)分別監(jiān)聽了它們的信道瓦胎,直到該信道關(guān)閉芬萍。程序的其他地方不變,該程序同樣會輸出:

Final output 1536

關(guān)于信道還有一些其他的概念搔啊,比如緩沖信道(Buffered Channel)柬祠、工作池(Worker Pool)和 select。我們會在接下來的教程里專門介紹它們

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末负芋,一起剝皮案震驚了整個濱河市漫蛔,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌示罗,老刑警劉巖惩猫,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異蚜点,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)拌阴,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門绍绘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人迟赃,你說我怎么就攤上這事陪拘。” “怎么了纤壁?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵左刽,是天一觀的道長。 經(jīng)常有香客問我酌媒,道長欠痴,這世上最難降的妖魔是什么迄靠? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮喇辽,結(jié)果婚禮上掌挚,老公的妹妹穿的比我還像新娘。我一直安慰自己菩咨,他們只是感情好吠式,可當(dāng)我...
    茶點故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著抽米,像睡著了一般特占。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上云茸,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天是目,我揣著相機(jī)與錄音,去河邊找鬼查辩。 笑死胖笛,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的宜岛。 我是一名探鬼主播长踊,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼萍倡!你這毒婦竟也來了身弊?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤列敲,失蹤者是張志新(化名)和其女友劉穎阱佛,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戴而,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡凑术,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了所意。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片淮逊。...
    茶點故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖扶踊,靈堂內(nèi)的尸體忽然破棺而出泄鹏,到底是詐尸還是另有隱情,我是刑警寧澤秧耗,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布备籽,位于F島的核電站,受9級特大地震影響分井,放射性物質(zhì)發(fā)生泄漏车猬。R本人自食惡果不足惜霉猛,卻給世界環(huán)境...
    茶點故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望诈唬。 院中可真熱鬧韩脏,春花似錦、人聲如沸铸磅。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽阅仔。三九已至吹散,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間八酒,已是汗流浹背空民。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留羞迷,地道東北人界轩。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像衔瓮,于是被迫代替她去往敵國和親浊猾。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容