通道(channel)是Go 語言中一種特殊的資料類型,通道本身就是并發安全的,可以通過它在多個 goroutine 之間傳遞資料。通道是Go 語言程式設計理念:“Do not communicate by sharing memory; instead, share memory by communicating”(不要通過共享資料來通信,而應該通過通信來共享資料。)的完美實作,在并發程式設計中經常會遇到它。下面來介紹一下通道的使用方法。
目錄
- 通道的發送和接收
-
- 雙向通道
- 單向通道
- 通道阻塞
-
- 緩沖通道的阻塞
- 非緩沖通道
- 通道關閉
- select語句與通道
- 總結
通道的發送和接收
通道包括雙向通道和單向通道,這裡雙向通道隻的是支援發送和接收的通道,而單向通道是隻能發送或者隻能接收的通道。
雙向通道
使用make函數聲明并初始化一個通道:
-
是表示通道類型的關鍵字chan
-
表示該通道類型的元素類型string
-
表示該通道的容量為3,最多可以緩存3個元素值。3
一個通道相當于一個先進先出(FIFO)的隊列,使用操作符
<-
進行元素值的發送和接收:
接收元素值:
首先接收到的元素為先存入通道中的元素值,也就是先進先出:
package main
import "fmt"
func main() {
str1 := []string{"hello","world", "!"}
ch1 := make(chan string, len(str1))
for _, str := range str1 {
ch1 <- str
}
for i := 0; i < len(str1); i++ {
elem := <- ch1
fmt.Println(elem)
}
}
執行結果:
hello
world
!
單向通道
單向通道包括隻能發送的通道和隻能接收的通道:
var WriteChan = make(chan<- interface{}, 1) // 隻能發送不能接收的通道
var ReadChan = make(<-chan interface{}, 1) // 隻能接收不能發送的通道
單向通道的這種特性可以用來限制函數的輸入類型或者輸出類型,比如下面的例子限制了隻能從通道中接收元素值:
package main
import (
"fmt"
)
func OnlyReadChan(num int) <-chan int {
ch := make(chan int, 1)
ch <- num
close(ch)
return ch
}
func main() {
Chan1 := OnlyReadChan(6)
num := <- Chan1
fmt.Println(num)
}
執行結果:
通道阻塞
通道操作是并發安全的,在同一時刻,隻會執行對同一個通道的任意個發送操作中的某一個,直到這個元素值被完全複制進該通道之後,其他針對該通道的發送操作才可能被執行。接收操作也一樣。另外,對于通道中的同一個元素值來說,發送操作和接收操作之間也是互斥的。
發送操作和接收操作是原子操作,也就是說,發送操作絕不會出現隻複制了一部分的情況,要麼還沒有複制,要麼已經複制完畢。接收操作在準備好元素值的副本之後,一定會删除掉通道中的原值,絕不會出現通道中仍有殘留的情況。在進行發送操作和接收操作時,代碼會一直阻塞在那裡,完成操作後才會繼續執行後面的代碼。通道的發送操作和接收操作是很快的,那麼什麼情況下會出現長時間的阻塞呢?下面介紹幾種情況。
緩沖通道的阻塞
緩沖通道是容量大于0的通道,也就是可以緩存資料的通道。
1、發送阻塞
如果緩沖通道已經填滿,如果有goroutine繼續向該通道發送資料就會阻塞。請看下面的例子:
package main
func main() {
ch1 := make(chan int, 1)
ch1 <- 1
ch1 <- 2
}
執行結果:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
...........
如果通道可以接收資料(有元素被接收),通道會通知最先等待發送操作的 goroutine再次執行發送操作。
2、接收阻塞
類似的,如果通道已空,如果繼續進行接收操作就會被阻塞。
package main
func main() {
ch1 := make(chan int, 1)
<- ch1
}
執行結果:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
...........
非緩沖通道
非緩沖通道是容量為0的通道,不能緩存資料。
非緩沖通道的資料傳遞是同步的,發送操作或者接收操作在執行後就會阻塞,需要對應的接收操作或者發送操作執行才會繼續傳遞。由此可以看出緩沖通道使用的是異步方式進行資料傳遞。
package main
import (
"fmt"
)
func main() {
str1 := []string{"hello","world", "!"}
ch1 := make(chan string, 0)
go func() {
for _, str := range str1 {
ch1 <- str
}
}()
for i := 0; i < len(str1); i++ {
elem := <- ch1
fmt.Println(elem)
}
}
執行結果:
hello
world
!
上面的代碼中3個goroutine向通道寫了三次資料,必須有三次接收,不然會阻塞。
對值為nil的通道進行發送操作和接收操作也會發生阻塞:
var ch1 chan int
ch1 <- 1 // 阻塞
<-ch1 // 阻塞
通道關閉
可以使用close()方法來關閉通道,通道關閉後,不能再對通道進行發送操作,可以進行接收操作。
package main
import "fmt"
func main() {
ch1 := make(chan int, 1)
ch1 <- 1
close(ch1)
ele := <-ch1
fmt.Println(ele)
ch1 <- 2
}
執行結果:
1
panic: send on closed channel
goroutine 1 [running]:
.....
如果通道關閉時,裡面還有元素,進行接收操作時,傳回的通道關閉标志仍然為true:
package main
import "fmt"
func main() {
ch1 := make(chan int, 1)
ch1 <- 1
close(ch1)
ele1, statu1 := <-ch1
fmt.Println(ele1, statu1)
ele2, statu2 := <-ch1
fmt.Println(ele2, statu2)
}
執行結果:
1 true
0 false
由于通道的這種特性,可以讓發送方來關閉通道。前面的例子可以這樣寫:
package main
import (
"fmt"
)
func main() {
str1 := []string{"hello","world", "!"}
ch1 := make(chan string, 0)
go func() {
for _, str := range str1 {
ch1 <- str
}
close(ch1)
}()
for i := 0; i < len(str1); i++ {
elem := <- ch1
fmt.Println(elem)
}
}
另外,不能對關閉的通道再次關閉:
package main
// import "fmt"
func main() {
ch1 := make(chan int, 1)
ch1 <- 1
close(ch1)
close(ch1)
}
執行結果:
panic: close of closed channel
select語句與通道
select語句通常與通道聯用,它是專為通道而設計的。select語句執行時,一般隻有一個case表達式或者default語句會被運作。
package main
import "fmt"
func main() {
ch1 := make(chan int, 1)
num := 2
select {
case data := <-ch1:
fmt.Println("Read data: ", data)
case ch1 <- num:
fmt.Println("Write data: ", num)
default:
fmt.Println("No candidate case is selected!")
}
}
執行結果:
需要注意的是,如果沒有default預設分支,case表達式都沒有滿足條件,那麼select語句就會被阻塞,直到至少有一個case表達式滿足條件為止。
如果同時有多個分支滿足條件,會随機選擇一個分支執行
for語句與select語句聯用時,分支中的break語句隻能結束目前select語句的執行,而不會退出for循環。下面的代碼永遠不會退出循環:
package main
import "fmt"
func main() {
ch1 := make(chan int, 1)
for {
select {
case ch1 <- 6:
fmt.Println("Write data: 6")
case data := <-ch1:
fmt.Println(data)
break
}
}
}
解決方案是使用goto語句和标簽。
方法1:
package main
import "fmt"
func main() {
ch1 := make(chan int, 1)
num := 6
for {
select {
case ch1 <- num:
fmt.Println("Write data: ", num)
case data := <-ch1:
fmt.Println("Read data: ", data)
goto loop
}
}
loop:
fmt.Println(ch1)
}
執行結果:
Write data: 6
Read data: 6
0xc00000e0e0
方法2:
package main
import "fmt"
func main() {
ch1 := make(chan int, 1)
num := 6
loop:
for {
select {
case ch1 <- num:
fmt.Println("Write data: ", num)
case data := <-ch1:
fmt.Println("Read data: ", data)
break loop
}
}
fmt.Println(ch1)
}
執行結果:
Write data: 6
Read data: 6
0xc0000e4000
總結
本文主要介紹了通道的基本操作:初始化、發送、接收和關閉,要注意在什麼情況下會引起通道阻塞。select語句通常與通道聯用,介紹了分支的選擇規則以及for語句與select語句聯用時如何退出循環。
通道是 Go 語言并發程式設計的重要實作基礎,還是有必要掌握的。
--THE END--