Hi,大家好,我是明哥。
在自己學習 Golang 的這段時間里,我寫了詳細的學習筆記放在我的個人微信公眾號 《Go編程時光》,對於 Go 語言,我也算是個初學者,因此寫的東西應該會比較適合剛接觸的同學,如果你也是剛學習 Go 語言,不防關注一下,一起學習,一起成長。
我的在線博客:http://golang.iswbm.com
我的 Github:github.com/iswbm/GolangCodingTime
Go 語言之所以開始流行起來,很大一部分原因是因為它自帶的並發機制。
如果說 goroutine 是 Go語言程序的並發體的話,那么 channel(信道) 就是 它們之間的通信機制。channel,是一個可以讓一個 goroutine 與另一個 goroutine 傳輸信息的通道,我把他叫做信道,也有人將其翻譯成通道,二者都是一個概念。
信道,就是一個管道,連接多個goroutine程序 ,它是一種隊列式的數據結構,遵循先入先出的規則。
1. 信道的定義與使用
每個信道都只能傳遞一種數據類型的數據,所以在你聲明的時候,你得指定數據類型(string int 等等)
var 信道實例 chan 信道類型
// 定義容量為10的信道
var 信道實例 [10]chan 信道類型
聲明后的信道,其零值是nil,無法直接使用,必須配合make函進行初始化。
信道實例 = make(chan 信道類型)
亦或者,上面兩行可以合並成一句,以下我都使用這樣的方式進行信道的聲明
信道實例 := make(chan 信道類型)
假如我要創建一個可以傳輸int類型的信道,可以這樣子寫。
// 定義信道
pipline := make(chan int)
信道的數據操作,無非就兩種:發送數據與讀取數據
// 往信道中發送數據
pipline<- 200
// 從信道中取出數據,並賦值給mydata
mydata := <-pipline
信道用完了,可以對其進行關閉,避免有人一直在等待。但是你關閉信道后,接收方仍然可以從信道中取到數據,只是接收到的會永遠是 0。
close(pipline)
對一個已關閉的信道再關閉,是會報錯的。所以我們還要學會,如何判斷一個信道是否被關閉?
當從信道中讀取數據時,可以有多個返回值,其中第二個可以表示 信道是否被關閉,如果已經被關閉,ok 為 false,若還沒被關閉,ok 為true。
x, ok := <-pipline
2. 信道的容量與長度
一般創建信道都是使用 make 函數,make 函數接收兩個參數
- 第一個參數:必填,指定信道類型
- 第二個參數:選填,不填默認為0,指定信道的容量(可緩存多少數據)
對於信道的容量,很重要,這里要多說幾點:
- 當容量為0時,說明信道中不能存放數據,在發送數據時,必須要求立馬有人接收,否則會報錯。此時的信道稱之為無緩沖信道。
- 當容量為1時,說明信道只能緩存一個數據,若信道中已有一個數據,此時再往里發送數據,會造成程序阻塞。 利用這點可以利用信道來做鎖。
- 當容量大於1時,信道中可以存放多個數據,可以用於多個協程之間的通信管道,共享資源。
至此我們知道,信道就是一個容器。
若將它比做一個紙箱子
- 它可以裝10本書,代表其容量為10
- 當前只裝了1本書,代表其當前長度為1
信道的容量,可以使用 cap 函數獲取 ,而信道的長度,可以使用 len 長度獲取。
package main
import "fmt"
func main() {
pipline := make(chan int, 10)
fmt.Printf("信道可緩沖 %d 個數據\n", cap(pipline))
pipline<- 1
fmt.Printf("信道中當前有 %d 個數據", len(pipline))
}
輸出如下
信道可緩沖 10 個數據
信道中當前有 1 個數據
3. 緩沖信道與無緩沖信道
按照是否可緩沖數據可分為:緩沖信道 與 無緩沖信道
緩沖信道
允許信道里存儲一個或多個數據,這意味着,設置了緩沖區后,發送端和接收端可以處於異步的狀態。
pipline := make(chan int, 10)
無緩沖信道
在信道里無法存儲數據,這意味着,接收端必須先於發送端准備好,以確保你發送完數據后,有人立馬接收數據,否則發送端就會造成阻塞,原因很簡單,信道中無法存儲數據。也就是說發送端和接收端是同步運行的。
pipline := make(chan int)
// 或者
pipline := make(chan int, 0)
4. 雙向信道與單向信道
通常情況下,我們定義的信道都是雙向通道,可發送數據,也可以接收數據。
但有時候,我們希望對信道的數據流向做一些控制,比如這個信道只能接收數據或者這個信道只能發送數據。
因此,就有了 雙向信道 和 單向信道 兩種分類。
雙向信道
默認情況下你定義的信道都是雙向的,比如下面代碼
import (
"fmt"
"time"
)
func main() {
pipline := make(chan int)
go func() {
fmt.Println("准備發送數據: 100")
pipline <- 100
}()
go func() {
num := <-pipline
fmt.Printf("接收到的數據是: %d", num)
}()
// 主函數sleep,使得上面兩個goroutine有機會執行
time.Sleep(1)
}
單向信道
單向信道,可以細分為 只讀信道 和 只寫信道。
定義只讀信道
var pipline = make(chan int)
type Receiver = <-chan int // 關鍵代碼:定義別名類型
var receiver Receiver = pipline
定義只寫信道
var pipline = make(chan int)
type Sender = chan<- int // 關鍵代碼:定義別名類型
var sender Sender = pipline
仔細觀察,區別在於 <-
符號在關鍵字 chan
的左邊還是右邊。
<-chan
表示這個信道,只能從里發出數據,對於程序來說就是只讀chan<-
表示這個信道,只能從外面接收數據,對於程序來說就是只寫
有同學可能會問:為什么還要先聲明一個雙向信道,再定義單向通道呢?比如這樣寫
type Sender = chan<- int
sender := make(Sender)
代碼是沒問題,但是你要明白信道的意義是什么?(以下是我個人見解
信道本身就是為了傳輸數據而存在的,如果只有接收者或者只有發送者,那信道就變成了只入不出或者只出不入了嗎,沒什么用。所以只讀信道和只寫信道,唇亡齒寒,缺一不可。
當然了,若你往一個只讀信道中寫入數據 ,或者從一個只寫信道中讀取數據 ,都會出錯。
完整的示例代碼如下,供你參考:
import (
"fmt"
"time"
)
//定義只寫信道類型
type Sender = chan<- int
//定義只讀信道類型
type Receiver = <-chan int
func main() {
var pipline = make(chan int)
go func() {
var sender Sender = pipline
fmt.Println("准備發送數據: 100")
sender <- 100
}()
go func() {
var receiver Receiver = pipline
num := <-receiver
fmt.Printf("接收到的數據是: %d", num)
}()
// 主函數sleep,使得上面兩個goroutine有機會執行
time.Sleep(1)
}
5. 遍歷信道
遍歷信道,可以使用 for 搭配 range關鍵字,在range時,要確保信道是處於關閉狀態,否則循環會阻塞。
import "fmt"
func fibonacci(mychan chan int) {
n := cap(mychan)
x, y := 1, 1
for i := 0; i < n; i++ {
mychan <- x
x, y = y, x+y
}
// 記得 close 信道
// 不然主函數中遍歷完並不會結束,而是會阻塞。
close(mychan)
}
func main() {
pipline := make(chan int, 10)
go fibonacci(pipline)
for k := range pipline {
fmt.Println(k)
}
}
6. 用信道來做鎖
當信道里的數據量已經達到設定的容量時,此時再往里發送數據會阻塞整個程序。
利用這個特性,可以用當他來當程序的鎖。
示例如下,詳情可以看注釋
package main
import {
"fmt"
"time"
}
// 由於 x=x+1 不是原子操作
// 所以應避免多個協程對x進行操作
// 使用容量為1的信道可以達到鎖的效果
func increment(ch chan bool, x *int) {
ch <- true
*x = *x + 1
<- ch
}
func main() {
// 注意要設置容量為 1 的緩沖信道
pipline := make(chan bool, 1)
var x int
for i:=0;i<1000;i++{
go increment(pipline, &x)
}
// 確保所有的協程都已完成
// 以后會介紹一種更合適的方法(Mutex),這里暫時使用sleep
time.Sleep(3)
fmt.Println("x 的值:", x)
}
輸出如下
x 的值:1000
如果不加鎖,輸出會小於1000。
系列導讀
24. 超詳細解讀 Go Modules 前世今生及入門使用