Go語言?Channel通道詳解
一、通道介紹
單純地將函數(shù)并發(fā)執(zhí)行是沒有意義的。函數(shù)與函數(shù)間需要交換數(shù)據(jù)才能體現(xiàn)并發(fā)執(zhí)行函數(shù)的意義。
雖然可以使用共享內(nèi)存進(jìn)行數(shù)據(jù)交換,但是共享內(nèi)存在不同的 goroutine 中容易發(fā)生競態(tài)問題。為了保證數(shù)據(jù)交換的正確性,必須使用互斥量對內(nèi)存進(jìn)行加鎖,這種做法勢必造成性能問題。
go提倡使用通信的方法代替共享內(nèi)存,這里通信的方法就是使用通道(channel),如下圖所示。

在地鐵站、食堂、洗手間等公共場所人很多的情況下,大家養(yǎng)成了排隊的習(xí)慣,目的也是避免擁擠、插隊導(dǎo)致的低效的資源使用和交換過程。代碼與數(shù)據(jù)也是如此,多個 goroutine 為了爭搶數(shù)據(jù),勢必造成執(zhí)行的低效率,使用隊列的方式是最高效的,channel 就是一種隊列一樣的結(jié)構(gòu)。
Go 語言中的通道(channel)是一種特殊的類型。在任何時候,同時只能有一個 goroutine 訪問通道進(jìn)行發(fā)送和獲取數(shù)據(jù)。goroutine 間通過通道就可以通信。
通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規(guī)則,保證收發(fā)數(shù)據(jù)的順序。
1、聲明通道
var 變量 chan 元素類型 var ch1 chan int // 聲明一個傳遞整型的通道 var ch2 chan bool // 聲明一個傳遞布爾型的通道 var ch3 chan []int // 聲明一個傳遞int切片的通道
chan 類型的空值是 nil,聲明后需要配合 make 后才能使用。
所以通道只能傳輸一種類型的數(shù)據(jù),比如 chan int 或者 chan string,所有的類型都可以用于通道,空接口 interface{} 也可以。甚至可以(有時非常有用)創(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)建一個整型類型的通道
ch2 := make(chan interface{}) //創(chuàng)建一個空接口類型的通道, 可以存放任意格式
type Equip struct{ /* 一些字段 */ }
ch2 := make(chan *Equip) //創(chuàng)建Equip指針類型的通道, 可以存放*Equip二、channel操作
通道有發(fā)送(send)、接收(receive)和關(guān)閉(close)三種操作。發(fā)送和接收都使用<-符號。
通道創(chuàng)建后,就可以使用通道進(jìn)行發(fā)送和接收操作。
定義一個通道:
ch := make(chan int)
1、發(fā)送
將一個值發(fā)送到通道中。
ch <- 10 // 把10發(fā)送到ch中
2、接收
從一個通道中接收值。
x := <- ch // 從ch中接收值并賦值給變量x <-ch // 從ch中接收值,忽略結(jié)果
3、關(guān)閉
我們通過調(diào)用內(nèi)置的close函數(shù)來關(guān)閉通道
close(ch)
關(guān)于關(guān)閉通道需要注意的事情是,只有在通知接收方goroutine所有的數(shù)據(jù)都發(fā)送完畢的時候才需要關(guān)閉通道
通道是可以被垃圾回收機(jī)制回收的,它和關(guān)閉文件是不一樣的,在結(jié)束操作之后關(guān)閉文件是必須要做的,但關(guān)閉通道不是必須的。
關(guān)閉后的通道有以下特點:
- 對一個關(guān)閉的通道再發(fā)送值就會導(dǎo)致panic。
- 對一個關(guān)閉的通道進(jìn)行接收會一直獲取值直到通道為空。(如果通道中還有數(shù)據(jù)的話)
- 對一個關(guān)閉的并且沒有值的通道執(zhí)行接收操作會得到對應(yīng)類型的零值。
- 關(guān)閉一個已經(jīng)關(guān)閉的通道會導(dǎo)致panic。
三、無緩沖通道

無緩沖的通道又稱為阻塞的通道
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("發(fā)送成功")
}
//這段代碼僅作為 描述無緩沖通道,實際會形成deadlock
//具體原因,看下述分析上面這段代碼能夠通過編譯,但是執(zhí)行的時候會出現(xiàn)以下錯誤:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
main.go:8 +0x54
上面的代碼會阻塞在ch <- 10這一行代碼形成死鎖,那如何解決這個問題呢?
一種方法是啟用一個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ā)送成功")
}因為我們使用ch := make(chan int)創(chuàng)建的是無緩沖的通道,無緩沖的通道只有在有人接收值的時候才能發(fā)送值。
無緩沖通道總結(jié):
1)無緩沖通道上的發(fā)送操作會阻塞,直到另一個goroutine在該通道上執(zhí)行接收操作,這時值才能發(fā)送成功,兩個goroutine將繼續(xù)執(zhí)行。
2)相反,如果接收操作先執(zhí)行,接收方的goroutine將阻塞,直到另一個goroutine在該通道上發(fā)送一個值。
3)使用無緩沖通道進(jìn)行通信將導(dǎo)致發(fā)送和接收的goroutine同步化。因此,無緩沖通道也被稱為同步通道。
四、有緩沖的通道
解決上面問題的方法還有一種就是使用有緩沖區(qū)的通道。

我們可以在使用make函數(shù)初始化通道的時候為其指定通道的容量
1、有緩沖通道聲明
通道實例 := make(chan 通道類型, 緩沖大小)
func main() {
ch := make(chan int, 1) // 創(chuàng)建一個容量為1的有緩沖區(qū)通道
ch <- 10
fmt.Println("發(fā)送成功")
}只要通道的容量大于零,那么該通道就是有緩沖的通道,通道的容量表示通道中能存放元素的數(shù)量。
2、阻塞條件
帶緩沖通道在很多特性上和無緩沖通道是類似的。無緩沖通道可以看作是長度永遠(yuǎn)為 0 的帶緩沖通道。因此根據(jù)這個特性,帶緩沖通道在下面列舉的情況下依然會發(fā)生阻塞:
- 帶緩沖通道被填滿時,嘗試再次發(fā)送數(shù)據(jù)時發(fā)生阻塞。
- 帶緩沖通道為空時,嘗試接收數(shù)據(jù)時發(fā)生阻塞。
為什么對通道要限制長度而不提供無限長度的通道?
我們知道通道(channel)是在兩個 goroutine 間通信的橋梁。使用 goroutine 的代碼必然有一方提供數(shù)據(jù),一方消費數(shù)據(jù)。當(dāng)提供數(shù)據(jù)一方的數(shù)據(jù)供給速度大于消費方的數(shù)據(jù)處理速度時,如果通道不限制長度,那么內(nèi)存將不斷膨脹直到應(yīng)用崩潰。
因此,限制通道的長度有利于約束數(shù)據(jù)提供方的供給速度,供給數(shù)據(jù)量必須在消費方處理量+通道長度的范圍內(nèi),才能正常地處理數(shù)據(jù)。
五、循環(huán)讀取信道
上面的代碼一個一個地去讀取信道簡直太費事了,Go語言允許我們使用range來讀取信道:
func main() {
ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
for v := range ch {
fmt.Println(v)
}
}
//deadline如果你執(zhí)行了上面的代碼,會報死鎖錯誤的,原因是range不等到信道關(guān)閉是不會結(jié)束讀取的。也就是如果 緩沖信道干涸了,那么range就會阻塞當(dāng)前goroutine, 所以死鎖咯。那么,我們試著避免這種情況,比較容易想到的是讀到信道為空的時候就結(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ù)據(jù),這個例子 是因為我們只在ch中存了數(shù)據(jù),現(xiàn)在一個一個往外取,信道大小是遞減的。另一個方式是顯式地關(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)閉的信道會禁止數(shù)據(jù)流入, 是只讀的。我們?nèi)匀豢梢詮年P(guān)閉的信道中取出數(shù)據(jù),但是不能再寫入數(shù)據(jù)了。
六、關(guān)閉通道
可以通過內(nèi)置的close()函數(shù)關(guān)閉channel(如果你的管道不往里存值或者取值的時候一定記得關(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)通過通道發(fā)送有限的數(shù)據(jù)時,我們可以通過close函數(shù)關(guān)閉通道來告知從該通道接收值的goroutine停止等待。
當(dāng)通道被關(guān)閉時,往該通道發(fā)送值會引發(fā)panic,從該通道里接收的值一直都是類型零值。那如何判斷一個通道是否被關(guān)閉了呢?
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 開啟goroutine將0~100的數(shù)發(fā)送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 開啟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)閉后會退出for range循環(huán)
fmt.Println(i)
}
}說明:在知道通道的一些阻塞情況后,為了防止deadlock ,可以使用更友好的方式從通道中讀取數(shù)據(jù)
if i, ok := <-ch1 ;ok{
...
}七、單向通道
有的時候我們會將通道作為參數(shù)在多個任務(wù)函數(shù)間傳遞,很多時候我們在不同的任務(wù)函數(shù)中使用通道都會對其進(jìn)行限制,比如限制通道在函數(shù)中只能發(fā)送或只能接收。
var 通道實例 chan<- 元素類型 // 只能發(fā)送通道 var 通道實例 <-chan 元素類型 // 只能接收通道
//往通道中寫
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語言 Channel通道詳解的文章就介紹到這了,更多相關(guān)Go Channel通道內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang實現(xiàn)多存儲驅(qū)動設(shè)計SDK案例
這篇文章主要介紹了Golang實現(xiàn)多存儲驅(qū)動設(shè)計SDK案例,Gocache是一個基于Go語言編寫的多存儲驅(qū)動的緩存擴(kuò)展組件,更多具體內(nèi)容感興趣的小伙伴可以參考一下2022-09-09
安裝GoLang環(huán)境和開發(fā)工具的圖文教程
Go是一門由Google開發(fā)的編程語言,GoLand的安裝非常簡單,本文主要介紹了安裝GoLang環(huán)境和開發(fā)工具的圖文教程,具有一定的參考價值,感興趣的可以了解一下2023-09-09
VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題及處理方法
這篇文章主要介紹了VSCode Golang dlv調(diào)試數(shù)據(jù)截斷問題,本文通過實例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-06-06
使用docker構(gòu)建golang線上部署環(huán)境的步驟詳解
這篇文章主要介紹了使用docker構(gòu)建golang線上部署環(huán)境的步驟,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-11-11

