信道(channel)(又稱為通道) ,,就是一個(gè)管道,,可以想像成 Go 協(xié)程之間通信的管道,。它是一種隊(duì)列式的數(shù)據(jù)結(jié)構(gòu),,遵循先入先出的規(guī)則。
每個(gè)信道都只能傳遞一種數(shù)據(jù)類型的數(shù)據(jù),,在你聲明的時(shí)候,,我們要指定信道的類型。chan Type
表示 Type
類型的信道,。信道的零值為 nil
,。
var channel_name chan channel_type
下面的語(yǔ)句聲明了一個(gè)類型為 int
的信道 a
,該信道 a
的值為 nil
,。
var a chan int
聲明完信道后,,信道的值為 nil
,,我們不能直接使用,,必須先使用 make
函數(shù)對(duì)信道進(jìn)行初始化操作。
channel_name = make(chan channel_type)
使用下面的語(yǔ)句我們可以對(duì)上面聲明過(guò)的信道 a
進(jìn)行初始化:
a = make(chan int)
這樣,,我們就已經(jīng)定義好了一個(gè) int
類型的信道 a
,。
當(dāng)然,,也可以使用下面的簡(jiǎn)短聲明語(yǔ)句一次性定義一個(gè)信道:
a := make(chan int)
往信道發(fā)送數(shù)據(jù)使用的是下面的語(yǔ)法:
// 把 data 數(shù)據(jù)發(fā)送到 channel_name 信道中
// 即把 data 數(shù)據(jù)寫入到 channel_name 信道中
channel_name <- data
從信道接收數(shù)據(jù)使用的是下面的語(yǔ)法:
// 從 channel_name 信道中接收數(shù)據(jù)到 value
// 即從 channel_name 信道中讀取數(shù)據(jù)到 value
value := <- channel_name
信道旁的箭頭方向指定了是發(fā)送數(shù)據(jù)還是接收數(shù)據(jù)。箭頭指向信道,,代表數(shù)據(jù)寫入到信道中,;箭頭往信道指向外,代表從信道讀數(shù)據(jù)出去,。
下面的例子演示了信道的使用:
package main
import (
"fmt"
)
func John(c chan string) {
// 往信道傳入數(shù)據(jù) "John: Hi"
c <- "John: Hi"
}
func main() {
// 創(chuàng)建一個(gè)信道
c := make(chan string)
// 打印 "Mary: Hello"
fmt.Println("Mary: Hello")
// 開啟協(xié)程
go John(c)
// 從信道接收數(shù)據(jù)
rec := <- c
// 打印從信道接收到的數(shù)據(jù)
fmt.Println(rec)
// 打印 "Mary: Nice to meet you"
fmt.Println("Mary: Nice to meet you")
}
該程序模仿了兩個(gè)人見面的場(chǎng)景,,在 main
函數(shù)中,創(chuàng)建了一個(gè)信道,,在 main
函數(shù)中先打印了 Mary: Hello
,,然后開啟協(xié)程運(yùn)行 John
函數(shù),而 main
函數(shù)通過(guò)協(xié)程接收數(shù)據(jù),,主協(xié)程發(fā)生了阻塞,,等待信道 c
發(fā)送的數(shù)據(jù),在函數(shù)中,,數(shù)據(jù) John: Hi
傳入信道中,,當(dāng)寫入完成時(shí),主協(xié)程接收了數(shù)據(jù),,解除了阻塞狀態(tài),,打印出從信道接收到的數(shù)據(jù) John: Hi
,最后打印 Mary: Nice to meet you
,。運(yùn)行該程序輸出如下:
Mary: Hello
John: Hi
Mary: Nice to meet you
從上面的例子我們知道,,如果從信道接收數(shù)據(jù)沒接收完主協(xié)程是不會(huì)繼續(xù)執(zhí)行下去的。當(dāng)把數(shù)據(jù)發(fā)送到信道時(shí),,程序控制會(huì)在發(fā)送數(shù)據(jù)的語(yǔ)句處發(fā)生阻塞,,直到有其它 Go 協(xié)程從信道讀取到數(shù)據(jù),才會(huì)解除阻塞。與此類似,,當(dāng)讀取信道的數(shù)據(jù)時(shí),,如果沒有其它的協(xié)程把數(shù)據(jù)寫入到這個(gè)信道,那么讀取過(guò)程就會(huì)一直阻塞著,。
對(duì)于一個(gè)已經(jīng)使用完畢的信道,,我們要將其進(jìn)行關(guān)閉。
close(channel_name)
這里要注意,,對(duì)于一個(gè)已經(jīng)關(guān)閉的信道如果再次關(guān)閉會(huì)導(dǎo)致報(bào)錯(cuò),,我們可以在接收數(shù)據(jù)時(shí),判斷信道是否已經(jīng)關(guān)閉,,從信道讀取數(shù)據(jù)返回的第二個(gè)值表示信道是否沒被關(guān)閉,,如果已經(jīng)關(guān)閉,返回值為 false
,;如果還未關(guān)閉,,返回值為 true
。
value, ok := <- channel_name
我們?cè)谇懊嬷v過(guò) make
函數(shù)是可以接收兩個(gè)參數(shù)的,,同理,,創(chuàng)建信道可以傳入第二個(gè)參數(shù)——容量。
- 當(dāng)容量為
0
時(shí),,說(shuō)明信道中不能存放數(shù)據(jù),,在發(fā)送數(shù)據(jù)時(shí),必須要求立馬有人接收,,否則會(huì)報(bào)錯(cuò),。此時(shí)的信道稱之為無(wú)緩沖信道。 - 當(dāng)容量為
1
時(shí),,說(shuō)明信道只能緩存一個(gè)數(shù)據(jù),,若信道中已有一個(gè)數(shù)據(jù),此時(shí)再往里發(fā)送數(shù)據(jù),,會(huì)造成程序阻塞,。利用這點(diǎn)可以利用信道來(lái)做鎖。 - 當(dāng)容量大于
1
時(shí),,信道中可以存放多個(gè)數(shù)據(jù),可以用于多個(gè)協(xié)程之間的通信管道,,共享資源,。
既然信道有容量和長(zhǎng)度,那么我們可以通過(guò) cap
函數(shù)和 len
函數(shù)獲取信道的容量和長(zhǎng)度,。
package main
import (
"fmt"
)
func main() {
// 創(chuàng)建一個(gè)信道
c := make(chan int, 3)
fmt.Println("初始化后:")
fmt.Println("cap =", cap(c))
fmt.Println("len =", len(c))
c <- 1
c <- 2
fmt.Println("傳入兩個(gè)數(shù)后:")
fmt.Println("cap =", cap(c))
fmt.Println("len =", len(c))
<- c
fmt.Println("取出一個(gè)數(shù)后:")
fmt.Println("cap =", cap(c))
fmt.Println("len =", len(c))
}
程序中 <- c
通過(guò)信道接收數(shù)據(jù)但沒有存入變量中也是合法的,,運(yùn)行該程序后輸出如下:
初始化后:
cap = 3
len = 0
傳入兩個(gè)數(shù)后:
cap = 3
len = 2
取出一個(gè)數(shù)后:
cap = 3
len = 1
按照是否可緩沖數(shù)據(jù)可分為:緩沖信道 與 無(wú)緩沖信道 。
無(wú)緩沖信道在信道里無(wú)法存儲(chǔ)數(shù)據(jù),接收端必須先于發(fā)送端準(zhǔn)備好,,以確保你發(fā)送完數(shù)據(jù)后,,有人立馬接收數(shù)據(jù),否則發(fā)送端就會(huì)造成阻塞,,原因很簡(jiǎn)單,,信道中無(wú)法存儲(chǔ)數(shù)據(jù)。也就是說(shuō)發(fā)送端和接收端是同步運(yùn)行的,。
c := make(chan int)
// 或者
c := make(chan int, 0)
緩沖信道允許信道里存儲(chǔ)一個(gè)或多個(gè)數(shù)據(jù),,設(shè)置緩沖區(qū)后,發(fā)送端和接收端可以處于異步的狀態(tài),。
c := make(chan int, 3)
到目前為止,,上面定義的都是雙向信道,既可以發(fā)送數(shù)據(jù)也可以接收數(shù)據(jù),。例如:
package main
import (
"fmt"
"time"
)
func main() {
// 創(chuàng)建一個(gè)信道
c := make(chan int)
// 發(fā)送數(shù)據(jù)
go func() {
fmt.Println("send: 1")
c <- 1
}()
// 接收數(shù)據(jù)
go func() {
n := <- c
fmt.Println("receive:", n)
}()
// 主協(xié)程休眠
time.Sleep(time.Millisecond)
}
運(yùn)行上面的程序輸出如下:
send: 1
receive: 1
單向信道只能發(fā)送或者接收數(shù)據(jù),。所以可以具體細(xì)分為只讀信道和只寫信道。
<-chan
表示這個(gè)信道,,只能發(fā)送出數(shù)據(jù),,即只讀:
// 定義只讀信道
c := make(chan int)
// 定義類型
type Receiver = <-chan int
var receiver Receiver = c
// 或者簡(jiǎn)單寫成下面的形式
type Receiver = <-chan int
receiver := make(Receiver)
chan<-
表示這個(gè)信道,只能接收數(shù)據(jù),,即只寫:
// 定義只寫信道
c := make(chan int)
// 定義類型
type Sender = chan<- int
var sender Sender = c
// 或者簡(jiǎn)單寫成下面的形式
type Sender = chan<- int
sender := make(Sender)
下面是一個(gè)例子:
package main
import (
"fmt"
"time"
)
// Sender 只寫信道類型
type Sender = chan<- string
// Receiver 只讀信道類型
type Receiver = <-chan string
func main() {
// 創(chuàng)建一個(gè)雙向信道
var pipe = make(chan string)
// 開啟一個(gè)協(xié)程
go func() {
// 只能寫信道
var sender Sender = pipe
fmt.Println("send: 2333")
sender <- "2333"
}()
// 開啟一個(gè)協(xié)程
go func() {
// 只能讀信道
var receiver Receiver = pipe
message := <-receiver
fmt.Println("receive: ", message)
}()
time.Sleep(time.Millisecond)
}
運(yùn)行上面的程序輸出如下:
send: 2333
receive: 2333
使用 for range
循環(huán)可以遍歷信道,,但在遍歷時(shí)要確保信道是處于關(guān)閉狀態(tài),否則循環(huán)會(huì)被阻塞,。
package main
import (
"fmt"
)
func f(c chan int) {
for i := 0; i < 10; i++ {
c <- i
}
// 記得要關(guān)閉信道
// 否則主協(xié)程遍歷完不會(huì)結(jié)束,,而會(huì)阻塞
close(c)
}
func main() {
// 創(chuàng)建一個(gè)信道
var pipe = make(chan int, 5)
go f(pipe)
for v := range pipe {
fmt.Println(v)
}
}
運(yùn)行上面的程序輸出數(shù)字 0
至 9
。
上面講過(guò),,當(dāng)信道容量為 1
時(shí),,說(shuō)明信道只能緩存一個(gè)數(shù)據(jù),若信道中已有一個(gè)數(shù)據(jù),,此時(shí)再往里發(fā)送數(shù)據(jù),,會(huì)造成程序阻塞。例如:
package main
import (
"fmt"
"time"
)
// 由于 x = x+1 不是原子操作
// 所以應(yīng)避免多個(gè)協(xié)程對(duì) x 進(jìn)行操作
// 使用容量為 1 的信道可以達(dá)到鎖的效果
func increment(ch chan bool, x *int) {
ch <- true
*x = *x + 1
<- ch
}
func main() {
pipe := make(chan bool, 1)
var x int
for i := 0; i < 10000; i++ {
go increment(pipe, &x)
}
time.Sleep(time.Millisecond)
fmt.Println("x =", x)
}
運(yùn)行該程序輸出如下:
x = 10000
但如果把容量改大,,例如 1000
,,可能輸出的數(shù)值會(huì)小于 10000
。
講完了鎖,,不得不提死鎖,。當(dāng) Go 協(xié)程給一個(gè)信道發(fā)送數(shù)據(jù)時(shí),照理說(shuō)會(huì)有其他 Go 協(xié)程來(lái)接收數(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 bool)
ch <- true
}
運(yùn)行上面的程序,,會(huì)觸發(fā) panic ,報(bào)下面的錯(cuò)誤:
fatal error: all goroutines are asleep - deadlock!
下面再來(lái)看看幾個(gè)例子,。
package main
import "fmt"
func main() {
ch := make(chan bool)
ch <- true
fmt.Println(<-ch)
}
上面的代碼你看起來(lái)可能覺得沒啥問(wèn)題,,創(chuàng)建一個(gè)信道,往里面寫入數(shù)據(jù),,再?gòu)睦锩孀x出數(shù)據(jù),,但運(yùn)行后會(huì)報(bào)同樣的錯(cuò)誤:
fatal error: all goroutines are asleep - deadlock!
那么為什么會(huì)出現(xiàn)死鎖呢?前面的基礎(chǔ)學(xué)的好的就不難想到使用 make
函數(shù)創(chuàng)建信道時(shí)默認(rèn)不傳遞第二個(gè)參數(shù),,信道中不能存放數(shù)據(jù),,在發(fā)送數(shù)據(jù)時(shí),必須要求立馬有人接收,,即該信道為無(wú)緩沖信道,。所以在接收者沒有準(zhǔn)備好前,發(fā)送操作會(huì)被阻塞,。
分析完引發(fā)異常的原因后,,我們可以將代碼修改如下,使用協(xié)程,,將接收者代碼放在另一個(gè)協(xié)程里:
package main
import (
"fmt"
"time"
)
func f(c chan bool) {
fmt.Println(<-c)
}
func main() {
ch := make(chan bool)
go f(ch)
ch <- true
time.Sleep(time.Millisecond)
}
當(dāng)然,,還有一種更加直接的方法,把無(wú)緩沖信道改為緩沖信道就行了:
package main
import "fmt"
func main() {
ch := make(chan bool, 1)
ch <- true
fmt.Println(<-ch)
}
有時(shí)候我們定義了信道的容量,,但信道里的容量已經(jīng)放不下新的數(shù)據(jù),,而沒有接收者接收數(shù)據(jù),就會(huì)造成阻塞,,而對(duì)于一個(gè)協(xié)程來(lái)說(shuō)就會(huì)造成死鎖:
package main
import "fmt"
func main() {
ch := make(chan bool, 1)
ch <- true
ch <- false
fmt.Println(<-ch)
}
同理,,當(dāng)程序一直在等待從信道里讀取數(shù)據(jù),而此時(shí)并沒有發(fā)送者會(huì)往信道中寫入數(shù)據(jù),。此時(shí)程序就會(huì)陷入死循環(huán),,造成死鎖。
參考文獻(xiàn):
[1] Alan A. A. Donovan; Brian W. Kernighan, Go 程序設(shè)計(jì)語(yǔ)言, Translated by 李道兵, 高博, 龐向才, 金鑫鑫 and 林齊斌, 機(jī)械工業(yè)出版社, 2017.