<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Go 經(jīng)典入門系列 22:Channel

          共 885字,需瀏覽 2分鐘

           ·

          2020-12-22 13:56

          點(diǎn)擊上方藍(lán)色“Go語言中文網(wǎng)”關(guān)注,每天一起學(xué) Go

          歡迎來到 Golang 系列教程[1]的第 22 篇。

          上一教程里,我們探討了如何使用 Go 協(xié)程(Goroutine)來實(shí)現(xiàn)并發(fā)。我們接著在本教程里學(xué)習(xí)信道(Channel),學(xué)習(xí)如何通過信道來實(shí)現(xiàn) Go 協(xié)程間的通信。

          什么是信道?

          信道可以想像成 Go 協(xié)程之間通信的管道。如同管道中的水會(huì)從一端流到另一端,通過使用信道,數(shù)據(jù)也可以從一端發(fā)送,在另一端接收。

          信道的聲明

          所有信道都關(guān)聯(lián)了一個(gè)類型。信道只能運(yùn)輸這種類型的數(shù)據(jù),而運(yùn)輸其他類型的數(shù)據(jù)都是非法的。

          chan T 表示 T 類型的信道。

          信道的零值為 nil。信道的零值沒有什么用,應(yīng)該像對(duì) map 和切片所做的那樣,用 make 來定義信道。

          下面編寫代碼,聲明一個(gè)信道。

          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)
          ?}
          }

          在線運(yùn)行程序[2]

          由于信道的零值為 nil,在第 6 行,信道 a 的值就是 nil。于是,程序執(zhí)行了 if 語句內(nèi)的語句,定義了信道 a。程序中 a 是一個(gè) int 類型的信道。該程序會(huì)輸出:

          channel?a?is?nil,?going?to?define?it
          Type?of?a?is?chan?int

          簡短聲明通常也是一種定義信道的簡潔有效的方法。

          a?:=?make(chan?int)

          這一行代碼同樣定義了一個(gè) int 類型的信道 a

          通過信道進(jìn)行發(fā)送和接收

          如下所示,該語法通過信道發(fā)送和接收數(shù)據(jù)。

          data?:=?<-?a?//?讀取信道?a
          a?<-?data?//?寫入信道?a

          信道旁的箭頭方向指定了是發(fā)送數(shù)據(jù)還是接收數(shù)據(jù)。

          在第一行,箭頭對(duì)于 a 來說是向外指的,因此我們讀取了信道 a 的值,并把該值存儲(chǔ)到變量 data

          在第二行,箭頭指向了 a,因此我們?cè)诎褦?shù)據(jù)寫入信道 a

          發(fā)送與接收默認(rèn)是阻塞的

          發(fā)送與接收默認(rèn)是阻塞的。這是什么意思?當(dāng)把數(shù)據(jù)發(fā)送到信道時(shí),程序控制會(huì)在發(fā)送數(shù)據(jù)的語句處發(fā)生阻塞,直到有其它 Go 協(xié)程從信道讀取到數(shù)據(jù),才會(huì)解除阻塞。與此類似,當(dāng)讀取信道的數(shù)據(jù)時(shí),如果沒有其它的協(xié)程把數(shù)據(jù)寫入到這個(gè)信道,那么讀取過程就會(huì)一直阻塞著。

          信道的這種特性能夠幫助 Go 協(xié)程之間進(jìn)行高效的通信,不需要用到其他編程語言常見的顯式鎖或條件變量。

          信道的代碼示例

          理論已經(jīng)夠了:)。接下來寫點(diǎn)代碼,看看協(xié)程之間通過信道是怎么通信的吧。

          我們其實(shí)可以重寫上章學(xué)習(xí) Go 協(xié)程[3] 時(shí)寫的程序,現(xiàn)在我們?cè)谶@里用上信道。

          首先引用前面教程里的程序。

          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")
          }

          在線運(yùn)行程序[4]

          這是上一篇的代碼。我們使用到了休眠,使 Go 主協(xié)程等待 hello 協(xié)程結(jié)束。如果你看不懂,建議你閱讀上一教程 Go 協(xié)程[5]

          我們接下來使用信道來重寫上面代碼。

          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")
          }

          在線運(yùn)行程序[6]

          在上述程序里,我們?cè)诘?12 行創(chuàng)建了一個(gè) bool 類型的信道 done,并把 done 作為參數(shù)傳遞給了 hello 協(xié)程。在第 14 行,我們通過信道 done 接收數(shù)據(jù)。這一行代碼發(fā)生了阻塞,除非有協(xié)程向 done 寫入數(shù)據(jù),否則程序不會(huì)跳到下一行代碼。于是,這就不需要用以前的 time.Sleep 來阻止 Go 主協(xié)程退出了。

          <-done 這行代碼通過協(xié)程(譯注:原文筆誤,信道)done 接收數(shù)據(jù),但并沒有使用數(shù)據(jù)或者把數(shù)據(jù)存儲(chǔ)到變量中。這完全是合法的。

          現(xiàn)在我們的 Go 主協(xié)程發(fā)生了阻塞,等待信道 done 發(fā)送的數(shù)據(jù)。該信道作為參數(shù)傳遞給了協(xié)程 hellohello 打印出 Hello world goroutine,接下來向 done 寫入數(shù)據(jù)。當(dāng)完成寫入時(shí),Go 主協(xié)程會(huì)通過信道 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")
          }

          在線運(yùn)行程序[7]

          在上面程序里,我們向 hello 函數(shù)里添加了 4 秒的休眠(第 10 行)。

          程序首先會(huì)打印 Main going to call hello go goroutine。接著會(huì)開啟 hello 協(xié)程,打印 hello go routine is going to sleep。打印完之后,hello 協(xié)程會(huì)休眠 4 秒鐘,而在這期間,主協(xié)程會(huì)在 <-done 這一行發(fā)生阻塞,等待來自信道 done 的數(shù)據(jù)。4 秒鐘之后,打印 hello go routine awake and going to write to done,接著再打印 Main received data

          信道的另一個(gè)示例

          我們?cè)倬帉懸粋€(gè)程序來更好地理解信道。該程序會(huì)計(jì)算一個(gè)數(shù)中每一位的平方和與立方和,然后把平方和與立方和相加并打印出來。

          例如,如果輸出是 123,該程序會(huì)如下計(jì)算輸出:

          squares?=?(1?*?1)?+?(2?*?2)?+?(3?*?3)
          cubes?=?(1?*?1?*?1)?+?(2?*?2?*?2)?+?(3?*?3?*?3)
          output?=?squares?+?cubes?=?50

          我們會(huì)這樣去構(gòu)建程序:在一個(gè)單獨(dú)的 Go 協(xié)程計(jì)算平方和,而在另一個(gè)協(xié)程計(jì)算立方和,最后在 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)
          }

          在線運(yùn)行程序[8]

          在第 7 行,函數(shù) calcSquares 計(jì)算一個(gè)數(shù)每位的平方和,并把結(jié)果發(fā)送給信道 squareop。與此類似,在第 17 行函數(shù) calcCubes 計(jì)算一個(gè)數(shù)每位的立方和,并把結(jié)果發(fā)送給信道 cubop

          這兩個(gè)函數(shù)分別在單獨(dú)的協(xié)程里運(yùn)行(第 31 行和第 32 行),每個(gè)函數(shù)都有傳遞信道的參數(shù),以便寫入數(shù)據(jù)。Go 主協(xié)程會(huì)在第 33 行等待兩個(gè)信道傳來的數(shù)據(jù)。一旦從兩個(gè)信道接收完數(shù)據(jù),數(shù)據(jù)就會(huì)存儲(chǔ)在變量 squarescubes 里,然后計(jì)算并打印出最后結(jié)果。該程序會(huì)輸出:

          Final?output?1536

          死鎖

          使用信道需要考慮的一個(gè)重點(diǎn)是死鎖。當(dāng) Go 協(xié)程給一個(gè)信道發(fā)送數(shù)據(jù)時(shí),照理說會(huì)有其他 Go 協(xié)程來接收數(shù)據(jù)。如果沒有的話,程序就會(huì)在運(yùn)行時(shí)觸發(fā) panic,形成死鎖。

          同理,當(dāng)有 Go 協(xié)程等著從一個(gè)信道接收數(shù)據(jù)時(shí),我們期望其他的 Go 協(xié)程會(huì)向該信道寫入數(shù)據(jù),要不然程序就會(huì)觸發(fā) panic。

          package?main

          func?main()?{
          ?ch?:=?make(chan?int)
          ?ch?<-?5
          }

          在線運(yùn)行程序[9]

          在上述程序中,我們創(chuàng)建了一個(gè)信道 ch,接著在下一行 ch <- 5,我們把 5 發(fā)送到這個(gè)信道。對(duì)于本程序,沒有其他的協(xié)程從 ch 接收數(shù)據(jù)。于是程序觸發(fā) panic,出現(xiàn)如下運(yùn)行時(shí)錯(cuò)誤。

          fatal?error:?all?goroutines?are?asleep?-?deadlock!

          goroutine?1?[chan?send]:
          main.main()
          ?/tmp/sandbox249677995/main.go:6?+0x80

          單向信道

          我們目前討論的信道都是雙向信道,即通過信道既能發(fā)送數(shù)據(jù),又能接收數(shù)據(jù)。其實(shí)也可以創(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)
          }

          在線運(yùn)行程序[10]

          上面程序的第 10 行,我們創(chuàng)建了唯送(Send Only)信道 sendchchan<- int 定義了唯送信道,因?yàn)榧^指向了 chan。在第 12 行,我們?cè)噲D通過唯送信道接收數(shù)據(jù),于是編譯器報(bào)錯(cuò):

          main.go:11:?invalid?operation:?<-sendch?(receive?from?send-only?type?chan<-?int)

          一切都很順利,只不過一個(gè)不能讀取數(shù)據(jù)的唯送信道究竟有什么意義呢?

          這就需要用到信道轉(zhuǎn)換(Channel Conversion)了。把一個(gè)雙向信道轉(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)
          }

          在線運(yùn)行程序[11]

          在上述程序的第 10 行,我們創(chuàng)建了一個(gè)雙向信道 cha1。在第 11 行 cha1 作為參數(shù)傳遞給了 sendData 協(xié)程。在第 5 行,函數(shù) sendData 里的參數(shù) sendch chan<- intcha1 轉(zhuǎn)換為一個(gè)唯送信道。于是該信道在 sendData 協(xié)程里是一個(gè)唯送信道,而在 Go 主協(xié)程里是一個(gè)雙向信道。該程序最終打印輸出 10

          關(guān)閉信道和使用 for range 遍歷信道

          數(shù)據(jù)發(fā)送方可以關(guān)閉信道,通知接收方這個(gè)信道不再有數(shù)據(jù)發(fā)送過來。

          當(dāng)從信道接收數(shù)據(jù)時(shí),接收方可以多用一個(gè)變量來檢查信道是否已經(jīng)關(guān)閉。

          v,?ok?:=?<-?ch

          上面的語句里,如果成功接收信道所發(fā)送的數(shù)據(jù),那么 ok 等于 true。而如果 ok 等于 false,說明我們?cè)噲D讀取一個(gè)關(guān)閉的通道。從關(guān)閉的信道讀取到的值會(huì)是該信道類型的零值。例如,當(dāng)信道是一個(gè) int 類型的信道時(shí),那么從關(guān)閉的信道讀取的值將會(huì)是 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)
          ?}
          }

          在線運(yùn)行程序[12]

          在上述的程序中,producer 協(xié)程會(huì)從 0 到 9 寫入信道 chn1,然后關(guān)閉該信道。主函數(shù)有一個(gè)無限的 for 循環(huán)(第 16 行),使用變量 ok(第 18 行)檢查信道是否已經(jīng)關(guān)閉。如果 ok 等于 false,說明信道已經(jīng)關(guān)閉,于是退出 for 循環(huán)。如果 ok 等于 true,會(huì)打印出接收到的值和 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)用于在一個(gè)信道關(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)
          ?}
          }

          在線運(yùn)行程序[13]

          在第 16 行,for range 循環(huán)從信道 ch 接收數(shù)據(jù),直到該信道關(guān)閉。一旦關(guān)閉了 ch,循環(huán)會(huì)自動(dòng)結(jié)束。該程序會(huì)輸出:

          Received??0
          Received??1
          Received??2
          Received??3
          Received??4
          Received??5
          Received??6
          Received??7
          Received??8
          Received??9

          我們可以使用 for range 循環(huán),重寫信道的另一個(gè)示例[14]這一節(jié)里面的代碼,提高代碼的可重用性。

          如果你仔細(xì)觀察這段代碼,會(huì)發(fā)現(xiàn)獲得一個(gè)數(shù)里的每位數(shù)的代碼在 calcSquarescalcCubes 兩個(gè)函數(shù)內(nèi)重復(fù)了。我們將把這段代碼抽離出來,放在一個(gè)單獨(dú)的函數(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)
          }

          在線運(yùn)行程序[15]

          上述程序里的 digits 函數(shù),包含了獲取一個(gè)數(shù)的每位數(shù)的邏輯,并且 calcSquarescalcCubes 兩個(gè)函數(shù)并發(fā)地調(diào)用了 digits。當(dāng)計(jì)算完數(shù)字里面的每一位數(shù)時(shí),第 13 行就會(huì)關(guān)閉信道。calcSquarescalcCubes 兩個(gè)協(xié)程使用 for range 循環(huán)分別監(jiān)聽了它們的信道,直到該信道關(guān)閉。程序的其他地方不變,該程序同樣會(huì)輸出:

          Final?output?1536

          本教程的內(nèi)容到此結(jié)束。關(guān)于信道還有一些其他的概念,比如緩沖信道(Buffered Channel)、工作池(Worker Pool)和 select。我們會(huì)在接下來的教程里專門介紹它們。感謝閱讀。祝你愉快。

          上一教程 - Go 協(xié)程

          下一教程 - 緩沖信道和工作池[16]


          via: https://golangbot.com/channels/

          作者:Nick Coghlan[17]譯者:Noluye[18]校對(duì):polaris1119[19]

          本文由 GCTT[20] 原創(chuàng)編譯,Go 中文網(wǎng)[21] 榮譽(yù)推出

          參考資料

          [1]

          Golang 系列教程: https://studygolang.com/subject/2

          [2]

          在線運(yùn)行程序: https://play.golang.org/p/QDtf6mvymD

          [3]

          Go 協(xié)程: https://studygolang.com/articles/12342

          [4]

          在線運(yùn)行程序: https://play.golang.org/p/U9ZZuSql8-

          [5]

          Go 協(xié)程: https://studygolang.com/articles/12342

          [6]

          在線運(yùn)行程序: https://play.golang.org/p/I8goKv6ZMF

          [7]

          在線運(yùn)行程序: https://play.golang.org/p/EejiO-yjUQ

          [8]

          在線運(yùn)行程序: https://play.golang.org/p/4RKr7_YO_B

          [9]

          在線運(yùn)行程序: https://play.golang.org/p/q1O5sNx4aW

          [10]

          在線運(yùn)行程序: https://play.golang.org/p/PRKHxM-iRK

          [11]

          在線運(yùn)行程序: https://play.golang.org/p/aqi_rJ1U8j

          [12]

          在線運(yùn)行程序: https://play.golang.org/p/XWmUKDA2Ri

          [13]

          在線運(yùn)行程序: https://play.golang.org/p/JJ3Ida1r_6

          [14]

          信道的另一個(gè)示例: #

          [15]

          在線運(yùn)行程序: https://play.golang.org/p/oL86W9Ui03

          [16]

          緩沖信道和工作池: https://studygolang.com/articles/12512

          [17]

          Nick Coghlan: https://golangbot.com/about/

          [18]

          Noluye: https://github.com/Noluye

          [19]

          polaris1119: https://github.com/polaris1119

          [20]

          GCTT: https://github.com/studygolang/GCTT

          [21]

          Go 中文網(wǎng): https://studygolang.com/



          推薦閱讀


          福利

          我為大家整理了一份從入門到進(jìn)階的Go學(xué)習(xí)資料禮包,包含學(xué)習(xí)建議:入門看什么,進(jìn)階看什么。關(guān)注公眾號(hào) 「polarisxu」,回復(fù) ebook 獲取;還可以回復(fù)「進(jìn)群」,和數(shù)萬 Gopher 交流學(xué)習(xí)。

          瀏覽 48
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  成人欧美A | 亚洲欧美日韩性爱 | 成人开心网 | 亚洲激情区 | 特色黄色片|