channel#
單純地將函數並發執行是沒有意義的。函數與函數之間需要交換數據才能體現並發執行函數的意義。
雖然可以使用共享內存進行數據交換,但是共享內存在不同的 goroutine 中容易發生競態問題。為了保證數據交換的正確性,必須使用互斥量對內存進行加固,這種做法勢必造成性能問題。
Go 語言的並發模型是 CSP(Communicating Sequential Processes),體長通過通信共享內存而不是通過共享內存實現通信。
如果說 goroutine 是 Go 程序並發的執行體,channel 就是它們之間的連接。channel 是可以讓一個 goroutine 發送特定值到另一個 goroutine 的通信機制。
Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先進先出原則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是生命 channel 的時候需要為其指定元素類型。
channel 類型#
channel 是一種類型,一種引用類型。聲明通道類型的格式如下:
var 變量 chan 元素類型
舉幾個例子:
var ch1 chan int // 聲明一個傳遞整型的通道
var ch2 chan bool // 聲明一個傳遞布爾型的通道
var ch3 chan []int // 聲明一個傳遞int切片的通道
創建 channel#
通道是引用類型,通道類型的空值是 nil。
var ch chan int
fmt.Println(ch) // nil
聲明的通道需要用 make 函數初始化才能使用。
創建 channel 的格式如下:
make(chan 元素類型,[緩衝大小])
channel 的緩衝大小是可選的
ch4 := make(chan int)
ch5 := make(chan bool)
ch6 := make(chan []int)
channel 操作#
通道有發送(send)、接收(receive)和關閉(close)三種操作
發送和接收都使用 <- 符號。
現在我們先使用以下語句定義一個通道:
ch := make(chan int)
發送#
將一個值發送到通道中。
ch <- 10
接收#
從一個通道中接收值
x := <- ch // 從ch中接收值並賦值給變量x
<-ch // 從ch中接收值,忽略結果
關閉#
我們通過調用內置的 close 函數來關閉通道
close(ch)
關於關閉通道需要注意的事情是,只有在通知接收方 goroutine 所有的數據都發送完成的時候才需要關閉通道。通道是可以被垃圾回收機制回收的,它和關閉文件是不一樣的,在結束操作之後關閉文件是必須要做的,但關閉通道不是必須的。
關閉後的通道有以下特點:
對一個關閉的通道再發送值就會導致panic。
對一個關閉的通道進行接收會一直獲取值直到通道為空。
對一個關閉的並且沒有值的通道執行接收操作會得到對應類型的零值。
關閉一個已經關閉的通道會導致panic。
無緩衝的通道#
無緩衝的通道又稱為阻塞的通道。我們來看一下下面的代碼
func main() {
ch := make(chan int)
ch <- 10
fmt.Println("發送成功")
}
上面那段代碼能夠通過編譯,但是執行的時候會出現以下錯誤
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
.../src/github.com/pprof/studygo/day06/channel02/main.go:8 +0x54
為什麼會出現 deadlock 錯誤呢?
因為我們使用 ch := make (chan int) 創建的是無緩衝的通道,無緩衝的通道只有在有人接收值的時候才能發送值。就像你住的小區沒有快遞櫃和代收點,快遞員給你打電話必須要把這個物品送到你手上,簡單來說就是無緩衝的通道必須要有接收才能發送。
上面的代碼會阻塞在 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("發送成功")
}
有緩衝的通道#
解決上面問題的方法還有一種就是使用有緩衝區的通道。
我們可以在使用 make 函數初始化通道的時候為其指定通道的容量,例如:
func main() {
ch := make(chan int, 1) // 創建一個容量為1的有緩衝區通道
ch <- 10
fmt.Println("發送成功")
}
只要通道的容量大於零,那麼該通道就是有緩衝的通道,通道的容量表示通道中能存放元素的數量。就像你小區的快遞櫃只有這麼多格子,滿了就裝不下,阻塞了,等別人取走一個快遞員就可以放一個。
close()#
可以通過內置的 close () 函數關閉 channel(如果你的管道不往裡面存值或者取值的時候一定記得關閉管道)
package main
import "fmt"
func main() {
c := make(chan int)
go func() {
for i := 1; i < 5; i++ {
c <- 1
}
close(c)
}()
for {
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main結束")
}
如何優雅的從通道循環取值#
當通過通道發送有限的數據時,我們可以通過 close 函數關閉通道來告知從該通道接收值的 goroutine 停止等待。當通道被關閉時,往該通道發送值會引發 panic,從該通道裡接收的值一直都是類型零值。那如何判斷一個通道是否被關閉了呢?
我們來看下面這個例子:
// channel 練習
func main() {
ch1 := make(chan int)
ch2 := make(chan int)
// 開啟goroutine將0~100的數發送到ch1中
go func() {
for i := 0; i < 100; i++ {
ch1 <- i
}
close(ch1)
}()
// 開啟goroutine從ch1中接收值,並將該值的平方發送到ch2中
go func() {
for {
i, ok := <-ch1 // 通道關閉後再取值ok=false
if !ok {
break
}
ch2 <- i * i
}
close(ch2)
}()
// 在主goroutine中從ch2中接收值打印
for i := range ch2 { // 通道關閉後會退出for range循環
fmt.Println(i)
}
}
從上面的例子中我們看到有兩種方式在接收值的時候判斷通道是否被關閉,我們通常使用的是 for range 的方式。
單向通道#
有時候我們會將通道作為參數在多個任務函數間傳遞,很多時候我們在不同的任務函數中使用通道都會對其進行限制,比如限制通道在函數中只能發送或只能接收。
Go 語言中提供了單向通道來處理這種情況。例如,我們把上面的例子改造如下:
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)
}
其中,
chan <- int 是一個只能發送的通道,可以發送但是不能接收;
<-chan int 是一個只能接收的通道,可以接收但是不能發送。
在函數傳參及任何賦值操作中將雙向通道轉換為單向通道是可以的,但反過來不可以。
常見異常#
注意:關閉已經關閉的 channel 也會引發 panic。