Go語(yǔ)言?Channel通道詳解
一、通道介紹
單純地將函數(shù)并發(fā)執(zhí)行是沒(méi)有意義的。函數(shù)與函數(shù)間需要交換數(shù)據(jù)才能體現(xiàn)并發(fā)執(zhí)行函數(shù)的意義。
雖然可以使用共享內(nèi)存進(jìn)行數(shù)據(jù)交換,但是共享內(nèi)存在不同的 goroutine 中容易發(fā)生競(jìng)態(tài)問(wèn)題。為了保證數(shù)據(jù)交換的正確性,必須使用互斥量對(duì)內(nèi)存進(jìn)行加鎖,這種做法勢(shì)必造成性能問(wèn)題。
go提倡使用通信的方法代替共享內(nèi)存,這里通信的方法就是使用通道(channel),如下圖所示。
在地鐵站、食堂、洗手間等公共場(chǎng)所人很多的情況下,大家養(yǎng)成了排隊(duì)的習(xí)慣,目的也是避免擁擠、插隊(duì)導(dǎo)致的低效的資源使用和交換過(guò)程。代碼與數(shù)據(jù)也是如此,多個(gè) goroutine 為了爭(zhēng)搶數(shù)據(jù),勢(shì)必造成執(zhí)行的低效率,使用隊(duì)列的方式是最高效的,channel 就是一種隊(duì)列一樣的結(jié)構(gòu)。
Go 語(yǔ)言中的通道(channel)是一種特殊的類型。在任何時(shí)候,同時(shí)只能有一個(gè) goroutine 訪問(wèn)通道進(jìn)行發(fā)送和獲取數(shù)據(jù)。goroutine 間通過(guò)通道就可以通信。
通道像一個(gè)傳送帶或者隊(duì)列,總是遵循先入先出(First In First Out)的規(guī)則,保證收發(fā)數(shù)據(jù)的順序。
1、聲明通道
var 變量 chan 元素類型 var ch1 chan int // 聲明一個(gè)傳遞整型的通道 var ch2 chan bool // 聲明一個(gè)傳遞布爾型的通道 var ch3 chan []int // 聲明一個(gè)傳遞int切片的通道
chan 類型的空值是 nil,聲明后需要配合 make 后才能使用。
所以通道只能傳輸一種類型的數(shù)據(jù),比如 chan int 或者 chan string,所有的類型都可以用于通道,空接口 interface{} 也可以。甚至可以(有時(shí)非常有用)創(chuàng)建通道的通道。
2、創(chuàng)建通道
通道是引用類型,需要使用 make 進(jìn)行創(chuàng)建(分配內(nèi)存),格式如下:
var ch1 chan string ch1 = make(chan string) //或者使用短類型 ch1 := make(chan string)
示例
ch1 := make(chan int) //創(chuàng)建一個(gè)整型類型的通道 ch2 := make(chan interface{}) //創(chuàng)建一個(gè)空接口類型的通道, 可以存放任意格式 type Equip struct{ /* 一些字段 */ } ch2 := make(chan *Equip) //創(chuàng)建Equip指針類型的通道, 可以存放*Equip
二、channel操作
通道有發(fā)送(send)、接收(receive)和關(guān)閉(close)三種操作。發(fā)送和接收都使用<-符號(hào)。
通道創(chuàng)建后,就可以使用通道進(jìn)行發(fā)送和接收操作。
定義一個(gè)通道:
ch := make(chan int)
1、發(fā)送
將一個(gè)值發(fā)送到通道中。
ch <- 10 // 把10發(fā)送到ch中
2、接收
從一個(gè)通道中接收值。
x := <- ch // 從ch中接收值并賦值給變量x <-ch // 從ch中接收值,忽略結(jié)果
3、關(guān)閉
我們通過(guò)調(diào)用內(nèi)置的close函數(shù)來(lái)關(guān)閉通道
close(ch)
關(guān)于關(guān)閉通道需要注意的事情是,只有在通知接收方goroutine所有的數(shù)據(jù)都發(fā)送完畢的時(shí)候才需要關(guān)閉通道
通道是可以被垃圾回收機(jī)制回收的,它和關(guān)閉文件是不一樣的,在結(jié)束操作之后關(guān)閉文件是必須要做的,但關(guān)閉通道不是必須的。
關(guān)閉后的通道有以下特點(diǎn):
- 對(duì)一個(gè)關(guān)閉的通道再發(fā)送值就會(huì)導(dǎo)致panic。
- 對(duì)一個(gè)關(guān)閉的通道進(jìn)行接收會(huì)一直獲取值直到通道為空。(如果通道中還有數(shù)據(jù)的話)
- 對(duì)一個(gè)關(guān)閉的并且沒(méi)有值的通道執(zhí)行接收操作會(huì)得到對(duì)應(yīng)類型的零值。
- 關(guān)閉一個(gè)已經(jīng)關(guān)閉的通道會(huì)導(dǎo)致panic。
三、無(wú)緩沖通道
無(wú)緩沖的通道又稱為阻塞的通道
func main() { ch := make(chan int) ch <- 10 fmt.Println("發(fā)送成功") } //這段代碼僅作為 描述無(wú)緩沖通道,實(shí)際會(huì)形成deadlock //具體原因,看下述分析
上面這段代碼能夠通過(guò)編譯,但是執(zhí)行的時(shí)候會(huì)出現(xiàn)以下錯(cuò)誤:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
main.go:8 +0x54
上面的代碼會(huì)阻塞在ch <- 10這一行代碼形成死鎖,那如何解決這個(gè)問(wèn)題呢?
一種方法是啟用一個(gè)goroutine去接收值,例如:
func recv(c chan int) { ret := <-c fmt.Println("接收成功", ret) } func main() { ch := make(chan int) go recv(ch) // 啟用goroutine從通道接收值 ch <- 10 fmt.Println("發(fā)送成功") }
因?yàn)槲覀兪褂胏h := make(chan int)創(chuàng)建的是無(wú)緩沖的通道,無(wú)緩沖的通道只有在有人接收值的時(shí)候才能發(fā)送值。
無(wú)緩沖通道總結(jié):
1)無(wú)緩沖通道上的發(fā)送操作會(huì)阻塞,直到另一個(gè)goroutine在該通道上執(zhí)行接收操作,這時(shí)值才能發(fā)送成功,兩個(gè)goroutine將繼續(xù)執(zhí)行。
2)相反,如果接收操作先執(zhí)行,接收方的goroutine將阻塞,直到另一個(gè)goroutine在該通道上發(fā)送一個(gè)值。
3)使用無(wú)緩沖通道進(jìn)行通信將導(dǎo)致發(fā)送和接收的goroutine同步化。因此,無(wú)緩沖通道也被稱為同步通道。
四、有緩沖的通道
解決上面問(wèn)題的方法還有一種就是使用有緩沖區(qū)的通道。
我們可以在使用make函數(shù)初始化通道的時(shí)候?yàn)槠渲付ㄍǖ赖娜萘?/p>
1、有緩沖通道聲明
通道實(shí)例 := make(chan 通道類型, 緩沖大小) func main() { ch := make(chan int, 1) // 創(chuàng)建一個(gè)容量為1的有緩沖區(qū)通道 ch <- 10 fmt.Println("發(fā)送成功") }
只要通道的容量大于零,那么該通道就是有緩沖的通道,通道的容量表示通道中能存放元素的數(shù)量。
2、阻塞條件
帶緩沖通道在很多特性上和無(wú)緩沖通道是類似的。無(wú)緩沖通道可以看作是長(zhǎng)度永遠(yuǎn)為 0 的帶緩沖通道。因此根據(jù)這個(gè)特性,帶緩沖通道在下面列舉的情況下依然會(huì)發(fā)生阻塞:
- 帶緩沖通道被填滿時(shí),嘗試再次發(fā)送數(shù)據(jù)時(shí)發(fā)生阻塞。
- 帶緩沖通道為空時(shí),嘗試接收數(shù)據(jù)時(shí)發(fā)生阻塞。
為什么對(duì)通道要限制長(zhǎng)度而不提供無(wú)限長(zhǎng)度的通道?
我們知道通道(channel)是在兩個(gè) goroutine 間通信的橋梁。使用 goroutine 的代碼必然有一方提供數(shù)據(jù),一方消費(fèi)數(shù)據(jù)。當(dāng)提供數(shù)據(jù)一方的數(shù)據(jù)供給速度大于消費(fèi)方的數(shù)據(jù)處理速度時(shí),如果通道不限制長(zhǎng)度,那么內(nèi)存將不斷膨脹直到應(yīng)用崩潰。
因此,限制通道的長(zhǎng)度有利于約束數(shù)據(jù)提供方的供給速度,供給數(shù)據(jù)量必須在消費(fèi)方處理量+通道長(zhǎng)度的范圍內(nèi),才能正常地處理數(shù)據(jù)。
五、循環(huán)讀取信道
上面的代碼一個(gè)一個(gè)地去讀取信道簡(jiǎn)直太費(fèi)事了,Go語(yǔ)言允許我們使用range來(lái)讀取信道:
func main() { ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch { fmt.Println(v) } } //deadline
如果你執(zhí)行了上面的代碼,會(huì)報(bào)死鎖錯(cuò)誤的,原因是range不等到信道關(guān)閉是不會(huì)結(jié)束讀取的。也就是如果 緩沖信道干涸了,那么range就會(huì)阻塞當(dāng)前goroutine, 所以死鎖咯。那么,我們?cè)囍苊膺@種情況,比較容易想到的是讀到信道為空的時(shí)候就結(jié)束讀取
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 for v := range ch { fmt.Println(v) if len(ch) <= 0 { // 如果現(xiàn)有數(shù)據(jù)量為0,跳出循環(huán) break } }
以上的方法是可以正常輸出的,但是注意檢查信道大小的方法不能在信道存取都在發(fā)生的時(shí)候用于取出所有數(shù)據(jù),這個(gè)例子 是因?yàn)槲覀冎辉赾h中存了數(shù)據(jù),現(xiàn)在一個(gè)一個(gè)往外取,信道大小是遞減的。另一個(gè)方式是顯式地關(guān)閉信道:
ch := make(chan int, 3) ch <- 1 ch <- 2 ch <- 3 // 顯式地關(guān)閉信道 close(ch) for v := range ch { fmt.Println(v) }
被關(guān)閉的信道會(huì)禁止數(shù)據(jù)流入, 是只讀的。我們?nèi)匀豢梢詮年P(guān)閉的信道中取出數(shù)據(jù),但是不能再寫(xiě)入數(shù)據(jù)了。
六、關(guān)閉通道
可以通過(guò)內(nèi)置的close()函數(shù)關(guān)閉channel(如果你的管道不往里存值或者取值的時(shí)候一定記得關(guān)閉管道)
package main import "fmt" func main() { c := make(chan int) go func() { for i := 0; i < 5; i++ { c <- i } close(c) }() for { if data, ok := <-c; ok { fmt.Println(data) } else { break } } fmt.Println("main結(jié)束") }
判斷通道是否關(guān)閉?
當(dāng)通過(guò)通道發(fā)送有限的數(shù)據(jù)時(shí),我們可以通過(guò)close函數(shù)關(guān)閉通道來(lái)告知從該通道接收值的goroutine停止等待。
當(dāng)通道被關(guān)閉時(shí),往該通道發(fā)送值會(huì)引發(fā)panic,從該通道里接收的值一直都是類型零值。那如何判斷一個(gè)通道是否被關(guān)閉了呢?
func main() { ch1 := make(chan int) ch2 := make(chan int) // 開(kāi)啟goroutine將0~100的數(shù)發(fā)送到ch1中 go func() { for i := 0; i < 100; i++ { ch1 <- i } close(ch1) }() // 開(kāi)啟goroutine從ch1中接收值,并將該值的平方發(fā)送到ch2中 go func() { for { // 通道關(guān)閉后再取值ok=false i, ok := <-ch1 if !ok { break } ch2 <- i * i } close(ch2) }() // 在主goroutine中從ch2中接收值打印 for i := range ch2 { // 通道關(guān)閉后會(huì)退出for range循環(huán) fmt.Println(i) } }
說(shuō)明:在知道通道的一些阻塞情況后,為了防止deadlock ,可以使用更友好的方式從通道中讀取數(shù)據(jù)
if i, ok := <-ch1 ;ok{ ... }
七、單向通道
有的時(shí)候我們會(huì)將通道作為參數(shù)在多個(gè)任務(wù)函數(shù)間傳遞,很多時(shí)候我們?cè)诓煌娜蝿?wù)函數(shù)中使用通道都會(huì)對(duì)其進(jìn)行限制,比如限制通道在函數(shù)中只能發(fā)送或只能接收。
var 通道實(shí)例 chan<- 元素類型 // 只能發(fā)送通道 var 通道實(shí)例 <-chan 元素類型 // 只能接收通道
//往通道中寫(xiě) func counter(out chan<- int) { for i := 0; i < 100; i++ { out <- i } close(out) } func squarer(out chan<- int, in <-chan int) { for i := range in { out <- i * i } close(out) } //從通道中讀 func printer(in <-chan int) { for i := range in { fmt.Println(i) } } func main() { ch1 := make(chan int) ch2 := make(chan int) go counter(ch1) go squarer(ch2, ch1) printer(ch2) }
總結(jié)
到此這篇關(guān)于Go語(yǔ)言 Channel通道詳解的文章就介紹到這了,更多相關(guān)Go Channel通道內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang實(shí)現(xiàn)多存儲(chǔ)驅(qū)動(dòng)設(shè)計(jì)SDK案例
這篇文章主要介紹了Golang實(shí)現(xiàn)多存儲(chǔ)驅(qū)動(dòng)設(shè)計(jì)SDK案例,Gocache是一個(gè)基于Go語(yǔ)言編寫(xiě)的多存儲(chǔ)驅(qū)動(dòng)的緩存擴(kuò)展組件,更多具體內(nèi)容感興趣的小伙伴可以參考一下2022-09-09安裝GoLang環(huán)境和開(kāi)發(fā)工具的圖文教程
Go是一門(mén)由Google開(kāi)發(fā)的編程語(yǔ)言,GoLand的安裝非常簡(jiǎn)單,本文主要介紹了安裝GoLang環(huán)境和開(kāi)發(fā)工具的圖文教程,具有一定的參考價(jià)值,感興趣的可以了解一下2023-09-09一文教你打造一個(gè)簡(jiǎn)易的Golang日志庫(kù)
這篇文章主要為大家詳細(xì)介紹了如何使用不超過(guò)130行的代碼,通過(guò)一系列g(shù)olang的特性,來(lái)打造一個(gè)簡(jiǎn)易的golang日志庫(kù),感興趣的小伙伴可以了解一下2023-06-06Go語(yǔ)言特點(diǎn)及基本數(shù)據(jù)類型使用詳解
這篇文章主要為大家介紹了Go語(yǔ)言特點(diǎn)及基本數(shù)據(jù)類型使用詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-03-03VSCode Golang dlv調(diào)試數(shù)據(jù)截?cái)鄦?wèn)題及處理方法
這篇文章主要介紹了VSCode Golang dlv調(diào)試數(shù)據(jù)截?cái)鄦?wèn)題,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-06-06使用docker構(gòu)建golang線上部署環(huán)境的步驟詳解
這篇文章主要介紹了使用docker構(gòu)建golang線上部署環(huán)境的步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11