這是『就要學習 Go 語言』系列的第 22 篇分享文章安全
上篇文章講了關於協程的一些用法,好比如何建立協程、匿名協程等。這篇文章咱們來說講信道。 信道是協程之間通訊的管道,從一端發送數據,另外一端接收數據。函數
使用信道以前須要聲明,有兩種方式:oop
var c chan int // 方式一
c := make(chan int) // 方式二
複製代碼
使用關鍵字 chan 建立信道,聲明時有類型,代表信道只容許該類型的數據傳輸。信道的零值爲 nil。方式一就聲明瞭 nil 信道。nil 信道沒什麼做用,既不能發送數據也不能接受數據。方式二使用 make 函數建立了可用的信道 c。學習
func main() {
c := make(chan int)
fmt.Printf("c Type is %T\n",c)
fmt.Printf("c Value is %v\n",c)
}
複製代碼
輸出:ui
c Type is chan int
c Value is 0xc000060060
複製代碼
上面的代碼建立了信道 c,並且只容許 int 型數據傳輸。通常將信道做爲參數傳遞給函數或者方法實現兩個協程之間通訊,有沒有注意到信道 c 的值是一個地址,傳參的時候直接使用 c 的值就能夠,而不用取址。spa
Go 提供了語法方便咱們操做信道:.net
c := make(chan int)
// 寫數據
c <- data
// 讀數據
variable <- c // 方式一
<- c // 方式二
複製代碼
讀寫數據注意信道的位置,信道在箭頭的左邊是寫數據,在右邊是從信道讀數據。上面的方式二讀數據是合理的,讀出來的數據丟棄不使用。 注意:信道操做默認是阻塞的,往信道里寫數據以後當前協程便阻塞,直到其餘協程將數據讀出。一個協程被信道操做阻塞後,Go 調度器會去調用其餘可用的協程,這樣程序就不會一直阻塞。信道的這種特性很是有用,接下來咱們就能夠看到。 咱們來溫習下上篇文章的一個例子:code
func printHello() {
fmt.Println("hello world goroutine")
}
func main() {
go printHello()
time.Sleep(1*time.Second)
fmt.Println("main goroutine")
}
複製代碼
這個例子 main() 協程使用了 time.Sleep() 函數休眠了 1s 等待 printHello() 執行完成。很黑科技,在生產環境絕對不能夠這樣用。咱們使用信道修改下:cdn
func printHello(c chan bool) {
fmt.Println("hello world goroutine")
<- c // 讀取信道的數據
}
func main() {
c := make(chan bool)
go printHello(c)
c <- true // main 協程阻塞
fmt.Println("main goroutine")
}
複製代碼
輸出:協程
hello world goroutine
main goroutine
複製代碼
上面的例子,main 協程建立完 printHello 協程以後,第 8 行往信道 c 寫數據,main 協程阻塞,Go 調度器調度可以使用 printHello 協程,從信道 c 讀出數據,main 協程接觸阻塞繼續運行。注意:讀取操做沒有阻塞是由於信道 c 已有可讀的數據,不然,讀取操做會阻塞。
前面提到過,讀/寫數據的時候信道會阻塞,調度器會去調度其餘可用的協程。問題來了,若是沒有其餘可用的協程會發生什麼狀況?沒錯,就會發生著名的死鎖。最簡單的狀況就是,只往信道寫數據。
func main() {
c := make(chan bool)
c <- true // 只寫不讀
fmt.Println("main goroutine")
}
複製代碼
報錯:
fatal error: all goroutines are asleep - deadlock!
複製代碼
同理,只讀不寫也會報一樣的錯誤。
發送數據的信道有能力選擇關閉信道,數據就不能傳輸。數據接收的時候能夠返回一個狀態判斷該信道是否關閉:
val, ok := <- channel
複製代碼
val 是接收的值,ok 標識信道是否關閉。爲 true 的話,該信道還能夠進行讀寫操做;爲 false 則標識信道關閉,數據不能傳輸。 使用內置函數 close() 關閉信道。
func printNums(ch chan int) {
for i := 0; i < 4; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go printNums(ch)
for {
v, ok := <-ch
if ok == false { // 經過 ok 判斷信道是否關閉
fmt.Println(v, ok)
break
}
fmt.Println(v, ok)
}
}
複製代碼
輸出:
0 true
1 true
2 true
3 true
0 false
複製代碼
printNums 協程寫完數據以後關閉了信道,在 main 協程裏對 ok 判斷,若爲 false 說明信道關閉,退出 for 循壞。從關閉的信道讀出來的值是對應類型的零值,上面最後一行的輸出值是 int 類型的零值 0。
使用 for 循環,須要手動判斷信道有沒有關閉。若是嫌煩的話,那就使用 for range 讀取信道吧,信道關閉,for range 自動退出。
func printNums(ch chan int) {
for i := 0; i < 4; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go printNums(ch)
for v := range ch {
fmt.Println(v)
}
}
複製代碼
輸出:
0
1
2
3
複製代碼
提一點,使用 for range 一個信道,發送完畢以後必須 close() 信道,否則發生死鎖。
以前建立的信道是無緩衝的,讀寫信道會立馬阻塞當前協程。對於緩衝信道,寫不會阻塞當前信道直到信道滿了,同理,讀操做也不會阻塞當前信道除非信道沒數據。建立帶緩衝的信道:
ch := make(chan type, capacity)
複製代碼
capacity 是緩衝大小,必須大於 0。 內置函數 len()、cap() 能夠計算信道的長度和容量。
func main() {
ch := make(chan int,3)
ch <- 7
ch <- 8
ch <- 9
//ch <- 10
// 註釋打開的話,協程阻塞,發生死鎖
會發生死鎖:信道已滿且沒有其餘可用信道讀取數據
fmt.Println("main stopped")
}
複製代碼
輸出:main stopped 建立了緩衝爲 3 的信道,寫入 3 個數據時信道不會阻塞。若是將第 7 行代碼註釋打開的話,此時信道已滿,協程阻塞,又沒有其餘可用協程讀數據,便發生死鎖。 再來看個例子:
func printNums(ch chan int) {
ch <- 7
ch <- 8
ch <- 9
fmt.Printf("channel len:%d,capacity:%d\n",len(ch),cap(ch))
fmt.Println("blocking...")
ch <- 10 // 阻塞
close(ch)
}
func main() {
ch := make(chan int,3)
go printNums(ch)
// 休眠 2s
time.Sleep(2*time.Second)
for v := range ch {
fmt.Println(v)
}
fmt.Println("main stopped")
}
複製代碼
輸出:
channel len:3,capacity:3
blocking...
7
8
9
10
main stopped
複製代碼
休眠 2s 的目的是讓信道寫滿數據發生阻塞,從打印結果能夠看出。2s 以後,主協程從信道讀取數據,信道容量有餘阻塞便解除,繼續寫數據。
若是緩衝信道是關閉狀態但有數據,仍然能夠讀取數據:
func main() {
ch := make(chan int,3)
ch <- 7
ch <- 8
//ch <- 9
close(ch)
for v := range ch {
fmt.Println(v)
}
fmt.Println("main stopped")
}
複製代碼
輸出:
7
8
main stopped
複製代碼
以前建立的都是雙向信道,既能發送數據也能接收數據。咱們還能夠建立單向信道,只發送或者只接收數據。 語法:
sch := make(chan<- int)
rch := make(<-chan int)
複製代碼
sch 是隻發送信道,rch 是隻接受信道。 這種單向信道有什麼用呢?咱們總不能只發不接或只接不發吧。這種信道主要用在信道做爲參數傳遞的時候,Go 提供了自動轉化,雙向轉單向。 重寫以前的例子:
func printNums(ch chan<- int) {
for i := 0; i < 4; i++ {
ch <- i
}
close(ch)
}
func main() {
ch := make(chan int)
go printNums(ch)
for v := range ch {
fmt.Println(v)
}
}
複製代碼
輸出:
0
1
2
3
複製代碼
main 協程中 ch 是一個雙向信道,printNums() 在接收參數的時候將 ch 自動轉成了單向信道,只發不收。但在 main 協程中,ch 仍然能夠接收數據。使用單向通道主要是能夠提升程序的類型安全性,程序不容易出錯。
信道是一類值,相似於 int、string 等,能夠像其餘值同樣在任何地方使用,好比做爲結構體成員、函數參數、函數返回值,甚至做爲另外一個通道的類型。咱們來看下使用通道做爲另外一個通道的數據類型。
func printWord(ch chan string) {
fmt.Println("Hello " + <-ch)
}
func productCh(ch chan chan string) {
c := make(chan string) // 建立 string type 信道
ch <- c // 傳輸信道
}
func main() {
// 建立 chan string 類型的信道
ch := make(chan chan string)
go productCh(ch)
// c 是 string type 的信道
c := <-ch
go printWord(c)
c <- "world"
fmt.Println("main stopped")
}
複製代碼
輸出:
Hello world
main stopped
複製代碼
但願這篇文章可以幫助你,Good day!
原創文章,若需轉載請註明出處!
歡迎掃碼關注公衆號「Golang來啦」或者移步 seekload.net ,查看更多精彩文章。
給你準備了學習 Go 語言相關書籍,公號後臺回覆【電子書】領取!