[TOC]
單純地將函數并發執行是沒有意義的。函數與函數間需要交換數據才能體現并發執行函數的意義。
雖然可以使用共享內存(全局變量)進行數據交換,但是共享內存在不同的 goroutine 中容易發生競態問題。為了保證數據交換的正確性,很多并發模型中必須使用互斥量對內存進行加鎖,這種做法勢必造成性能問題。
Go語言采用的并發模型是CSP(Communicating Sequential Processes),提倡 **通過通信共享內存** 而不是通過共享內存而實現通信。
如果說 goroutine 是Go程序并發的執行體,channel就是它們之間的連接。channel是可以讓一個 goroutine 發送特定值到另一個 goroutine 的通信機制。
Go 語言中的通道(channel)是一種特殊的類型。通道像一個傳送帶或者隊列,總是遵循先入先出(First In First Out)的規則,保證收發數據的順序。每一個通道都是一個具體類型的導管,也就是聲明channel的時候需要為其指定元素類型。
## channel 定義
`channel` 是 Go 語言中一種特有的類型。聲明通道類型變量的格式如下:
```go
var 變量名稱 chan 元素類型
```
- chan:是關鍵字
- 元素類型:是指通道中傳遞元素的類型
示例:
```go
var ch1 chan string // 傳遞字符串的通道
var ch2 chan int // 傳遞整型的通道
var ch3 chan []string // 傳遞字符串切片的通道
```
## channel 零值
未初始化的通道類型變量其默認零值是nil。
```go
var ch1 chan string // 傳遞字符串的通道
fmt.Printf("ch1: %v\n", ch1)
// 運行結果:
// ch1: <nil>
```
## 初始化 channel
聲明的通道類型變量需要使用內置的make函數初始化之后才能使用。具體格式如下:
```go
make(chan 元素類型, [緩沖大小])
```
> channel的緩沖大小是可選的。
示例
```go
ch2 := make(chan int)
ch3 := make(chan []string, 1)
```
## channel 操作
通道共有發送(send)、接收(receive)和關閉(close)三種操作。而發送和接收操作都使用<-符號。
```go
// 定義字符串通道
ch3 := make(chan string, 3)
// 發送數據到通道
ch3 <- "jiaxzeng"
fmt.Printf("ch3 len is: %v, cap is: %v\n", len(ch3), cap(ch3))
// 接收通道的數據
name := <-ch3
fmt.Printf("name: %v\n", name)
fmt.Printf("ch3 len is: %v, cap is: %v\n", len(ch3), cap(ch3))
// 關閉通道
close(ch3)
// 關閉通道,再嘗試接收
name = <-ch3
fmt.Printf("name: %v\n", name)
// 運行結果:
// ch3 len is: 1, cap is: 3
// name: jiaxzeng
// ch3 len is: 0, cap is: 3
// name:
```
**注意**:一個通道值是可以被垃圾回收掉的。通道通常由發送方執行關閉操作,并且只有在接收方明確等待通道關閉的信號時才需要執行關閉操作。它和關閉文件不一樣,通常在結束操作之后關閉文件是必須要做的,但關閉通道不是必須的。
>[info] 關閉后的通道有以下特點
> 1. 對一個關閉的通道再發送值就會導致 panic。
> 2. 對一個關閉的通道進行接收會一直獲取值直到通道為空。
> 3. 對一個關閉的并且沒有值的通道執行接收操作會得到對應類型的零值。
> 4. 關閉一個已經關閉的通道會導致 panic。
## 無緩沖 channel
無緩沖的通道又稱為阻塞的通道。
```go
// 定義字符串通道
ch4 := make(chan string)
// 發送數據到通道
ch4 <- "jiaxzeng"
// 報錯提示
// fatal error: all goroutines are asleep - deadlock!
// goroutine 1 [chan send]:
// main.main()
// /data/code/src/code.jiaxzeng.com/backend/study/9.concurrency/channel.go:35 +0x37
// exit status 2
```
`deadlock` 表示我們程序中的 goroutine 都被掛起導致程序死鎖了。為什么會出現deadlock錯誤呢?
因為我們使用 `ch4 := make(chan string)` 創建的是無緩沖的通道,無緩沖的通道只有在有接收方能夠接收值的時候才能發送成功,否則會一直處于等待發送的階段。同理,如果對一個無緩沖通道執行接收操作時,沒有任何向通道中發送值的操作那么也會導致接收操作阻塞。簡單來說就是無緩沖的通道必須有至少一個接收方才能發送成功。
上面的代碼會阻塞在`ch4 <- "jiaxzeng"`這一行代碼形成死鎖,那如何解決這個問題呢?
提前先創建好接收者即可
```go
func chanReceive(c chan string) {
name := <-c
fmt.Printf("name: %v\n", name)
}
func main(){
// 定義字符串通道
ch4 := make(chan string)
// 接收通道的值
go chanReceive(ch4)
// 發送數據到通道
ch4 <- "jiaxzeng"
// 關閉通道
close(ch4)
}
```
首先無緩沖通道 ch4 上的發送操作會阻塞,直到另一個 goroutine 在該通道上執行接收操作,這時字符串 `jiaxzeng` 才能發送成功,兩個 goroutine 將繼續執行。相反,如果接收操作先執行,接收方所在的 goroutine 將阻塞,直到 `main goroutine` 中向該通道發送字符串 `jiaxzeng` 。
使用無緩沖通道進行通信將導致發送和接收的 goroutine 同步化。因此,無緩沖通道也被稱為同步通道。
## 有緩沖 channel
還有另外一種解決上面死鎖問題的方法,那就是使用有緩沖區的通道。我們可以在使用 make 函數初始化通道時,可以為其指定通道的容量,例如:
```go
func chanReceive(ch chan int) {
i := <-ch
fmt.Printf("i: %v\n", i)
}
func main(){
// 定義整型通道
ch5 := make(chan int, 3)
// 發送數據
ch5 <- 5
// 關閉通道
close(ch5)
// 接收通道的數據
chanReceive(ch5)
}
// i: 5
```
只要通道的容量大于零,那么該通道就屬于有緩沖的通道,通道的容量表示通道中最大能存放的元素數量。當通道內已有元素數達到最大容量后,再向通道執行發送操作就會阻塞,除非有從通道執行接收操作。就像你小區的快遞柜只有那么個多格子,格子滿了就裝不下了,就阻塞了,等到別人取走一個快遞員就能往里面放一個。
我們可以使用內置的len函數獲取通道內元素的數量,使用cap函數獲取通道的容量,雖然我們很少會這么做。
## 判斷通道是否被關閉
當向通道中發送完數據時,我們可以通過close函數來關閉通道。當一個通道被關閉后,再往該通道發送值會引發 `panic` ,從該通道取值的操作會先取完通道中的值。通道內的值被接收完后再對通道執行接收操作得到的值會一直都是對應元素類型的零值。那我們如何判斷一個通道是否被關閉了呢?
對一個通道執行接收操作時支持使用如下多返回值模式,基于上面的函數修改...
```go
func chanReceive(ch chan int) {
// i := <-ch
// fmt.Printf("i: %v\n", i)
for {
// 注意,ok變量只有當通道數據都被接收且關閉才會返回false
// 如果發送數據后沒有關閉,這里就會發生panic
v, ok := <-ch
if ok {
fmt.Printf("v: %v\n", v)
} else {
fmt.Println("Read completion")
break
}
}
}
func main(){
// 定義通道
ch5 := make(chan int, 3)
// 發送數據到通道
ch5 <- 5
ch5 <- 10
// 關閉通道
// 先于通道接收,否則會出現panic
close(ch5)
// 接收通道數據
chanReceive(ch5)
}
// v: 5
// v: 10
// Read completion
```
## for range接收值
通常我們會選擇使用 for range 循環從通道中接收值,當通道被關閉后,會在通道內的所有值被接收完畢后會自動退出循環。上面那個示例我們使用 for range 改寫后會很簡潔。
```go
func chanReceive(ch chan int) {
/* for {
v, ok := <-ch
if ok {
fmt.Printf("v: %v\n", v)
} else {
fmt.Println("Read completion")
break
}
} */
for v := range ch {
fmt.Printf("v: %v\n", v)
}
}
```
>[warning] **注意**:目前Go語言中并沒有提供一個不對通道進行讀取操作就能判斷通道是否被關閉的方法。不能簡單的通過len(ch)操作來判斷通道是否被關閉。
## 單向通道
在某些場景下我們可能會將通道作為參數在多個任務函數間進行傳遞,通常我們會選擇在不同的任務函數中對通道的使用進行限制,比如限制通道在某個函數中只能執行發送或只能執行接收操作
單向通道標識符
```go
<- chan int // 只接收通道,只能接收不能發送
chan <- int // 只發送通道,只能發送不能接收
```
示例,生產者只使用發送通道,消費者只使用接收通道。
```go
func Producer() <-chan int {
ch := make(chan int, 3)
go func() {
for i := 0; i < 10; i++ {
ch <- i
}
close(ch)
}()
return ch
}
func Consumer(ch <-chan int) int {
sum := 0
for {
v, ok := <-ch
if ok {
// 通道數據的平方和
sum += v * v
} else {
break
}
}
return sum
}
func main() {
// Producer函數返回值是只接收的通道
ch6 := Producer()
// Consumer函數實參只能是單通道(接收),返回整型類型
sum := Consumer(ch6)
fmt.Printf("sum: %v\n", sum)
}
```
## 總結
下面的表格中總結了對不同狀態下的通道執行相應操作的結果。
| | nil | 沒值 | 有值 | 滿 |
| :-: | :-: | :-: | :-: | :-: |
| 發送 | 阻塞導致死鎖 | 成功 | 成功 | 發送阻塞 |
| 接收 | 阻塞導致死鎖 | 零值 | 成功 | 成功 |
| 關閉 | panic | 成功 | 成功 | 成功 |
- Golang簡介
- 開發環境
- Golang安裝
- 編輯器及快捷鍵
- vscode插件
- 第一個程序
- 基礎數據類型
- 變量及匿名變量
- 常量與iota
- 整型與浮點型
- 復數與布爾值
- 字符串
- 運算符
- 算術運算符
- 關系運算符
- 邏輯運算符
- 位運算符
- 賦值運算符
- 流程控制語句
- 獲取用戶輸入
- if分支語句
- for循環語句
- switch語句
- break_continue_goto語法
- 高階數據類型
- pointer指針
- array數組
- slice切片
- slice切片擴展
- map映射
- 函數
- 函數定義和調用
- 函數參數
- 函數返回值
- 作用域
- 函數形參傳遞
- 匿名函數
- 高階函數
- 閉包
- defer語句
- 內置函數
- fmt
- strconv
- strings
- time
- os
- io
- 文件操作
- 編碼
- 字符與字節
- 字符串
- 讀寫文件
- 結構體
- 類型別名和自定義類型
- 結構體聲明
- 結構體實例化
- 模擬構造函數
- 方法接收器
- 匿名字段
- 嵌套與繼承
- 序列化
- 接口
- 接口類型
- 值接收者和指針接收者
- 類型與接口對應關系
- 空接口
- 接口值
- 類型斷言
- 并發編程
- 基本概念
- goroutine
- channel
- select
- 并發安全
- 練習題
- 第三方庫
- Survey
- cobra