Go 学习笔记(24)— 并发(03)[通道特点、通道声明、通道发送/接收/关闭、单向通道]
1. 通道概念
chan 是 Go 語言里面的一個關鍵宇,是 channel 的簡寫,翻譯為中文就是通道。
goroutine 是 Go 語言里面的并發執行體,通道是 goroutine 之間通信和同步的重要組件。 Go 的哲學是
Don’t communicate by sharing memory; share memory by communicating. (不要通過共享內存來通信,而應該通過通信來共享內存。)這是作為 Go 語言的主要創造者之一的 Rob Pike 的至理名言。
Go 語言通過通道可以實現多個 goroutine 之間內存共享。
在 Go 語言里,你不僅可以使用原子函數和互斥鎖來保證對共享資源的安全訪問以及消除競爭狀態,還可以使用通道,通過發送和接收需要共享的資源,在 goroutine 之間做同步。
當一個資源需要在 goroutine 之間共享時,通道在 goroutine 之間架起了一個管道,并提供了確保同步交換數據的機制。
多個 goroutine 中, Go 語言使用通道( channel )進行通信,通道是一種內置的數據結構,可以讓用戶在不同的 goroutine 之間同步發送具有類型的消息。這讓編程模型更傾向于在 goroutine 之間發送消息,而不是讓多個 goroutine 爭奪同一個數據的使用權。
程序可以將需要并發的環節設計為生產者模式和消費者的模式,將數據放入通道。通道另外一端的代碼將這些數據進行并發計算并返回結果,如下圖所示。
生產者和消費者代碼示例:
// 整段代碼中,沒有線程創建,沒有線程池也沒有加鎖,
// 僅僅通過關鍵字 go 實現 goroutine,和通道實現數據交換。
package mainimport ("fmt""math/rand""time"
)// 數據生產者
func producer(header string, channel chan<- string) {// 無限循環, 不停地生產數據for {// 將隨機數和字符串格式化為字符串發送給通道channel <- fmt.Sprintf("%s: %v", header, rand.Int31())// 等待1秒time.Sleep(time.Second)}
}// 數據消費者
func customer(channel <-chan string) {// 不停地獲取數據for {// 從通道中取出數據, 此處會阻塞直到信道中返回數據message := <-channel// 打印數據fmt.Println(message)}
}
func main() {// 創建一個字符串類型的通道channel := make(chan string)// 創建producer()函數的并發goroutinego producer("cat", channel)go producer("dog", channel)// 數據消費函數customer(channel)
}
聲明通道時,需要指定將要被共享的數據的類型。可以通過通道共享內置類型、命名類型、結構類型和引用類型的值或者指針。
2. 通道特點
-
通道類型的值本身就是并發安全的,這也是
Go語言自帶的、唯一一個可以滿足并發安全性的類型。 -
一個通道相當于一個先進先出(FIFO)的隊列。也就是說,通道中的各個元素值都是嚴格地按照發送的順序排列的,先被發送通道的元素值一定會先被接收。
-
在任何時候,同時只能有一個
goroutine訪問通道進行發送和獲取數據。goroutine間通過通道就可以通信。 -
對于同一個通道,發送操作之間是互斥的,接收操作之間也是互斥的。
在同一時刻,
Go語言的運行時系統(以下簡稱運行時系統)只會執行對同一個通道的任意個發送操作中的某一個。
直到這個元素值被完全復制進該通道之后,其他針對該通道的發送操作才可能被執行。
類似的,在同一時刻,運行時系統也只會執行,對同一個通道的任意個接收操作中的某一個。
直到這個元素值完全被移出該通道之后,其他針對該通道的接收操作才可能被執行。
即使這些操作是并發執行的也是如此。這里所謂的并發執行,你可以這樣認為,多個代碼塊分別在不同的 goroutine 之中,并有機會在同一個時間段內被執行。
另外,對于通道中的同一個元素值來說,發送操作和接收操作之間也是互斥的。例如,雖然會出現,正在被復制進通道但還未復制完成的元素值,但是這時它絕不會被想接收它的一方看到和取走。
這里要注意的一個細節是,元素值從外界進入通道時會被復制。更具體地說,進入通道的并不是在接收操作符右邊的那個元素值,而是它的副本。
另一方面,元素值從通道進入外界時會被移動。這個移動操作實際上包含了兩步,第一步是生成正在通道中的這個元素值的副本,并準備給到接收方,第二步是刪除在通道中的這個元素值。
- 發送操作和接收操作中對元素值的處理都是不可分割的。
這里的“不可分割”的意思是,它們處理元素值時都是一氣呵成的,絕不會被打斷。
例如,發送操作要么還沒復制元素值,要么已經復制完畢,絕不會出現只復制了一部分的情況。
又例如,接收操作在準備好元素值的副本之后,一定會刪除掉通道中的原值,絕不會出現通道中仍有殘留的情況。
這既是為了保證通道中元素值的完整性,也是為了保證通道操作的唯一性。
對于通道中的同一個元素值來說,它只可能是某一個發送操作放入的,同時也只可能被某一個接收操作取出。
- 發送操作在完全完成之前會被阻塞。接收操作也是如此。
一般情況下,發送操作包括了“復制元素值”和“放置副本到通道內部”這兩個步驟。
在這兩個步驟完全完成之前,發起這個發送操作的那句代碼會一直阻塞在那里。
也就是說,在它之后的代碼不會有執行的機會,直到這句代碼的阻塞解除。
更細致地說,在通道完成發送操作之后,運行時系統會通知這句代碼所在的 goroutine,以使它去爭取繼續運行代碼的機會。
另外,接收操作通常包含了“復制通道內的元素值”“放置副本到接收方”“刪掉原值”三個步驟,也就是說通常,值進入通道時會被復制一次,然后出通道的時候依照通道內的那個值再被復制一次并給到接收方。在所有這些步驟完全完成之前,發起該操作的代碼也會一直阻塞,直到該代碼所在的 goroutine 收到了運行時系統的通知并重新獲得運行機會為止。
3. 通道聲明
Go 語言提倡使用通信的方法代替共享內存,當一個資源需要在 goroutine 之間共享時,通道在 goroutine 之間架起了一個管道,并提供了確保同步交換數據的機制。
一般 channel 的聲明形式為:
var chanName chan ElementType
與一般的變量聲明不同的地方僅僅是在類型之前加了 chan 關鍵字。ElementType 指定這個
channel 所能傳遞的元素類型。
聲明一個傳遞類型為 int 的 channel:
var ch chan int
或者使用內置函數 make 來創建一個通道,
// 無緩沖的整型通道
unbuffered := make(chan int)// 有緩沖的字符串通道
buffered := make(chan string, 10)
make 的第一個參數需要是關鍵字 chan ,之后跟著允許通道交換的數據的類型。如果創建的是一個有緩沖的通道,之后還需要在第二個參數指定這個通道的緩沖區的大小。
Go 提供內置函數 len 和 cap ,無緩沖的通道的 len 和 cap 都是 0,有緩沖的通道的 len 代表沒有被讀取的元素數, cap 代表整個通道的容量。
d1 := make(chan int)
d2 := make(chan int, 3)
d2 <- 1
fmt.Println(len(d1), cap(d1)) // 0 0
fmt.Println(len(d2), cap(d2)) // 1 3
無緩沖的通道既可以用于通信,也可以用于兩個goroutine 的同步,有緩沖的通道主要用于通信。
和 map 類似, channel 也對應一個 make 創建的底層數據結構的引用。當我們復制一個 channel 或用于函數參數傳遞時,我們只是拷貝了一個 channel 引用,因此調用者和被調用者將引用同一個 channel 對象。和其它的引用類型一樣, channel 的零值也是 nil 。
聲明通道時,需要指定將要被共享的數據的類型。可以通過通道共享內置類型、命名類型、結構類型和引用類型的值或者指針。
ch1 := make(chan int) // 創建一個整型類型的通道
ch2 := make(chan interface{}) // 創建一個空接口類型的通道, 可以存放任意格式type Equip struct{ /* 一些字段 */ }
ch2 := make(chan *Equip) // 創建Equip指針類型的通道, 可以存放*Equip
4. 通道操作
發送和接收兩個操作都使用 <- 運算符。在發送語句中, <- 運算符分割 channel 和要發送的值。在接收語句中, <- 運算符寫在 channel 對象之前。
4.1 通道發送數據
通道的發送使用特殊的操作符<-,將數據通過通道發送的格式為:
channelName <- value
其中:
- channelName:通過
make創建好的通道實例; - value:可以是變量、常量、表達式或者函數返回值等。值的類型必須與ch通道的元素類型一致;
使用 make 創建一個通道后,就可以使用<-向通道發送數據,代碼如下:
// 有緩沖的字符串通道,數據類型是字符串,包含一個 10 個值的緩沖區。
buffered := make(chan string, 10)// 通過通道發送一個字符串
buffered <- "Gopher"
4.2 通道發送阻塞
把數據往通道中發送時,如果接收方一直都沒有接收,那么發送操作將持續阻塞。
package mainfunc main() {// 創建一個整型通道ch := make(chan int) // 無緩沖的通道// 嘗試將0通過通道發送ch <- 0
}
輸出:
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:
報錯的意思是:運行時發現所有的 goroutine (包括main)都處于等待 goroutine 。也就是說所有 goroutine 中的 channel 并沒有形成發送和接收對應的代碼。
但是如果是有緩沖的通道時則不會發生阻塞,見下代碼:
package mainfunc main() {ch := make(chan int, 10) // 有緩沖的通道ch <- 0
}
4.3 通道接收數據
一個 channel 有發送和接受兩個主要操作,都是通信行為。一個發送語句將一個值從一個 goroutine 通過 channel 發送到另一個執行接收操作的 goroutine 。
通道接收同樣使用<-操作符,通道接收有如下特性:
① 通道的收發操作在兩個不同的 goroutine 間進行。
由于通道的數據在沒有接收方處理時,數據發送方會持續阻塞,因此通道的接收必定在另外一個 goroutine 中進行。
② 接收將持續阻塞直到發送方發送數據。
如果接收方接收時,通道中沒有發送方發送數據,接收方也會發生阻塞,直到發送方發送數據為止。
③ 每次接收一個元素。
通道一次只能接收一個數據元素。
通道接收數據有以下幾種寫法。
4.3.1 阻塞接收數據
阻塞模式接收數據時,將接收變量作為<-操作符的左值,格式如下:
data := <-ch
為了讓另一個 goroutine 可以從該通道里接收到這個字符串,我們依舊使用 <- 操作符,但這次是一元運算符,
// 有緩沖的字符串通道,數據類型是字符串,包含一個 10 個值的緩沖區。
buffered := make(chan string, 10)// 通過通道發送一個字符串
buffered <- "Gopher"
// 從通道接收一個字符串
value := <-buffered
當從通道里接收一個值或者指針時, <- 運算符在要操作的通道變量的左側。
4.3.2 非阻塞接收數據
使用非阻塞方式從通道接收數據時,語句不會發生阻塞,格式如下:
data, ok := <- ch
- data:表示接收到的數據。未接收到數據時,data 為通道類型的零值。
- ok:表示是否接收到數據。
非阻塞的通道接收方法可能造成高的 CPU 占用,因此使用非常少。如果需要實現接收超時檢測,可以配合 select 和計時器 channel 進行。
package mainimport ("errors""fmt""time"
)// 模擬RPC客戶端的請求和接收消息封裝
func RPCClient(ch chan string, req string) (string, error) {// 向服務器發送請求ch <- req// 等待服務器返回select {// 下面兩個通道操作同時開啟,那個先返回就先執行哪個后面的語句 case ack := <-ch: // 接收到服務器返回數據return ack, nilcase <-time.After(time.Second): // 超時return "", errors.New("Time out")}
}// 模擬RPC服務器端接收客戶端請求和回應
func RPCServer(ch chan string) {for {// 接收客戶端請求data := <-ch// 打印接收到的數據fmt.Println("server received:", data)time.Sleep(2 * time.Second)// 反饋給客戶端收到ch <- "roger"}
}func main() {// 創建一個無緩沖字符串通道ch := make(chan string)// 并發執行服務器邏輯go RPCServer(ch)// 客戶端請求數據和接收數據recv, err := RPCClient(ch, "hi")if err != nil {// 發生錯誤打印fmt.Println(err)} else {// 正常接收到數據fmt.Println("client received", recv)}
}
更多 select 操作請參考 https://blog.csdn.net/wohu1104/article/details/115497151
4.3.3 接收任意數據,忽略接收的數據
阻塞接收數據后,忽略從通道返回的數據,格式如下:
<-ch // a receive statement; result is discarded
執行該語句時將會發生阻塞,直到接收到數據,但接收到的數據會被忽略。這個方式實際上只是通過通道在 goroutine 間阻塞收發實現并發同步。
使用通道做并發同步的寫法,可以參考下面的例子:
package mainimport "fmt"func main() {// 構建一個通道ch := make(chan int)// 開啟一個并發匿名函數go func() {fmt.Println("start goroutine")// 通過通道通知main的goroutinech <- 0fmt.Println("exit goroutine")}()fmt.Println("wait goroutine")// 等待匿名goroutine<-chfmt.Println("all done")
}
輸出結果:
wait goroutine
start goroutine
exit goroutine
all done
4.3.4 循環接收
通道的數據接收可以借用 for range 語句進行多個元素的接收操作,格式如下:
for data := range ch {}
range 子句的迭代目標不能是一個發送通道,與試圖從發送通道接收元素值情況一樣,這會造成編譯錯誤。
for ... range 循環遍歷通道時,信道必須關閉,否則會引發 deadlock 錯誤。
迭代為 nil 的通道值會讓當前流程永遠阻塞在 for 語句上。
func main() {// 創建一個channelvar c chan intfor data := range c {fmt.Println(data)}
}
運行錯誤提示:
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan receive (nil chan)]:
main.main()/home/wohu/gocode/src/100.go:8 +0xa8
exit status 2
通道 ch 是可以進行遍歷的,遍歷的結果就是接收到的數據。數據類型就是通道的數據類型。通過 for 遍歷獲得的變量只有一個,即上面例子中的 data。
遍歷通道數據的例子請參考下面的代碼:
// 使用匿名函數
package mainimport ("fmt""time"
)func main() {// 構建一個通道ch := make(chan int)// 開啟一個并發匿名函數go func() {// 從3循環到0for i := 3; i >= 0; i-- {// 發送3到0之間的數值ch <- i// 每次發送完時等待time.Sleep(time.Second)}}()// 遍歷接收通道數據for data := range ch {// 打印通道數據fmt.Println(data)// 當遇到數據0時, 退出接收循環if data == 0 {break}}}
// 非匿名函數
package mainimport ("fmt""time"
)func getNumber(ch chan<- int) {// 從3循環到0for i := 3; i >= 0; i-- {// 發送3到0之間的數值ch <- i// 每次發送完時等待time.Sleep(time.Second)}
}
func main() {// 構建一個通道ch := make(chan int)// 開啟一個并發匿名函數go getNumber(ch)// 遍歷接收通道數據for data := range ch {// 打印通道數據fmt.Println(data)// 當遇到數據0時, 退出接收循環// 如果繼續發送,由于接收 goroutine 已經退出,沒有 goroutine 發送到通道,因此運行時將會觸發宕機報錯。if data == 0 {break}}}
5. 通道關閉
關閉通道一般都在發送端關閉。
手動關閉通道是個很好的習慣,而且也可以利用關的動作來給接收方傳遞一個信號。Go 的 GC 只會清理被分配到堆上的、不再有任何引用的對象。
channel 支持 close 操作,用于關閉 channel ,隨后對基于該 channel 的任何發送操作都將導致 panic 異常。
對一個已經被 close 過的 channel 進行接收操作依然可以接受到之前已經成功發送的數據;如果 channel 中已經沒有數據的話將產生一個零值的數據。
使用內置的 close 函數就可以關閉一個 channel :
close(ch)
如何判斷一個 channel 是否已經被關閉?我們可以使用 channel 的 receive 支持 multi-valued assignment (多值賦值),如
v, ok := <-ch
只需要看第二個 bool 返回值即可,如果返回值是 false 則表示 ch 已經被關閉。它可以用來檢查 channel 是否已經被關閉了。
5.1 向已關閉通道發送數據觸發 panic
被關閉的通道不會被置為 nil 。如果嘗試對已經關閉的通道進行發送,將會觸發 panic ,代碼如下:
package mainimport "fmt"func main() {// 創建一個整型的通道ch := make(chan int)// 關閉通道close(ch)// 打印通道的指針, 容量和長度fmt.Printf("ptr:%p cap:%d len:%d\n", ch, cap(ch), len(ch))// 給關閉的通道發送數據ch <- 1
}
輸出結果:
ptr:0xc000058060 cap:0 len:0
panic: send on closed channel
5.2 從已關閉的通道接收數據時將不會發生阻塞
從已經關閉的通道接收數據或者正在接收數據時,將會接收到通道類型的零值,然后停止阻塞并返回。代碼如下:
package mainimport "fmt"func main() {// 創建一個整型帶兩個緩沖的通道ch := make(chan int, 2)// 給通道放入兩個數據ch <- 0ch <- 1// 關閉緩沖close(ch)// 遍歷緩沖所有數據, 且多遍歷1個for i := 0; i < cap(ch)+1; i++ {// 從通道中取出數據v, ok := <-ch// 打印取出數據的狀態fmt.Println(v, ok)}
}
輸出結果:
0 true
1 true
0 false // 表示通道在關閉狀態下取出的值。0 表示這個通道的默認值,false 表示沒有獲取成功
6. 單向通道
Go 語言的類型系統提供了單方向的 channel 類型,顧名思義,單向 channel 只能用于發送或者接收數據。 channel 本身必然是同時支持讀寫的,否則根本沒法用。可以將 channel 隱式轉換為單向隊列,只收或只發。
單向通道的聲明格式:
單向 channel 變量的聲明非常簡單,只能發送的通道類型為chan<-,只能接收的通道類型為<-chan,格式如下:
var 通道實例 chan<- 元素類型 // 只能發送通道
var 通道實例 <-chan 元素類型 // 只能接收通道
通道必須用 make 初始化后才能使用,關于讀寫順序可以采用下面的方法聯想記憶。
讀在前,寫在后 <- chan 中的 <- 符號在 chan 前,因此為只讀通道;chan <- 中的 <- 符號在 chan 后,因此為 只寫通道
示例:
// 只能發不能收的通道。
var uselessChan = make(chan<- int, 1)
// 只能收不能發的通道。
var anotherUselessChan = make(<-chan int, 1)var ch1 chan int // ch1是一個正常的channel,不是單向的
var ch2 chan<- float64// ch2是單向channel,只用于寫float64數據
var ch3 <-chan int // ch3是單向channel,只用于讀取int數據
- 元素類型:通道包含的元素類型。
- 通道實例:聲明的通道變量。
使用示例:
c := make(chan int, 3)
var send chan<- int = c // send-only
var recv <-chan int = c // receive-only
send <- 1
// <-send // Error: receive from send-only type chan<- int
<-recv
// recv <- 2 // Error: send to receive-only type <-chan int
不能將單向 channel 轉換為普通 channel。
d := (chan int)(send) // Error: cannot convert type chan<- int to type chan int
d := (chan int)(recv) // Error: cannot convert type <-chan int to type chan int
記住:一般在通道的聲明時,都不會刻意聲明為單通道,這樣做會聲明一個只進不出,或者只出不進的單通道,沒有任何意義。
而是在函數的定義形參過程中指定通道的是發送通道還是接收通道。這樣做的目的適用于約束其他代碼的行為。
func SendInt(ch chan<- int) {ch <- rand.Intn(1000)
}
這個函數只接受一個 chan<- int 類型的參數。在這個函數中的代碼只能向參數 ch 發送元素值,而不能從它那里接收元素值。這就起到了約束函數行為的作用。
雙向 channel 轉化為單向 channel 之間進行轉換。示例如下:
ch4 := make(chan int)
ch5 := <-chan int(ch4) // ch5就是一個單向的讀取channel
ch6 := chan<- int(ch4) // ch6 是一個單向的寫入channel
單向通道使用示例:
package mainimport ("fmt""math/rand""time"
)// 數據生產者
func producer(header string, channel chan<- string) {// 無限循環,不停的生產數據for {// 將隨機數和字符串格式化為字符串發送到通道channel <- fmt.Sprintf("%s: %v", header, rand.Int31())// 等待1秒time.Sleep(time.Second)}
}// 數據消費者
func consumer(channel <-chan string) {// 不停的獲取數據for {// 從通道中取出數據,此處會阻塞直到信道中返回數據message := <-channel// 打印數據fmt.Println(message)}
}func main() {// 創建一個字符串類型的通道channel := make(chan string)// 創建producer函數的并發goroutinego producer("cat", channel)go producer("dog", channel)// 數據消費函數consumer(channel)
}
7. 通道示例
使用無緩沖通道往里面裝入數據時,裝入方將被阻塞,直到另外通道在另外一個 goroutine 中被取出。同樣,如果通道中沒有放入任何數據,接收方試圖從通道中獲取數據時,同樣也是阻塞。發送和接收的操作是同步完成的。
package mainimport "fmt"func printer(c chan int) {// 開始無限循環等待數據for {// 從channel中獲取一個數據data := <-c// 將0視為數據結束if data == 0 {break}// 打印數據fmt.Println(data)}// 通知main已經結束循環(我搞定了!)c <- 0
}func main() {// 創建一個channelc := make(chan int)// 并發執行printer, 傳入channelgo printer(c)for i := 1; i <= 10; i++ {// 將數據通過channel投送給printerc <- i}// 通知并發的printer結束循環(沒數據啦!)c <- 0// 等待printer結束(搞定喊我!)<-c
}
8. 發送操作和接收操作在什么時候可能被長時間的阻塞?
8.1 對緩沖通道而言
針對緩沖通道的情況。如果通道已滿,那么對它的所有發送操作都會被阻塞,直到通道中有元素值被接收走。
這時,通道會優先通知最早因此而等待的、那個發送操作所在的 goroutine,后者會再次執行發送操作。
由于發送操作在這種情況下被阻塞后,它們所在的 goroutine 會順序地進入通道內部的發送等待隊列,所以通知的順序總是公平的。
相對的,如果通道已空,那么對它的所有接收操作都會被阻塞,直到通道中有新的元素值出現。
這時,通道會通知最早等待的那個接收操作所在的 goroutine,并使它再次執行接收操作。
因此而等待的、所有接收操作所在的 goroutine,都會按照先后順序被放入通道內部的接收等待隊列。
8.2 對非緩沖通道而言
對于非緩沖通道,情況要簡單一些。無論是發送操作還是接收操作,一開始執行就會被阻塞,直到配對的操作也開始執行,才會繼續傳遞。
由此可見,非緩沖通道是在用同步的方式傳遞數據。也就是說,只有收發雙方對接上了,數據才會被傳遞。并且,數據是直接從發送方復制到接收方的,中間并不會用非緩沖通道做中轉。
相比之下,緩沖通道則在用異步的方式傳遞數據。在大多數情況下,緩沖通道會作為收發雙方的中間件。
正如前文所述,元素值會先從發送方復制到緩沖通道,之后再由緩沖通道復制給接收方。但是,當發送操作在執行的時候發現空的通道中,正好有等待的接收操作,那么它會直接把元素值復制給接收方。
以上說的都是在正確使用通道的前提下會發生的事情。下面我特別說明一下,由于錯誤使用通道而造成的阻塞。
對于值為 nil 的通道,不論它的具體類型是什么,對它的發送操作和接收操作都會永久地處于阻塞狀態。它們所屬的 goroutine 中的任何代碼,都不再會被執行。
注意,由于通道類型是引用類型,所以它的零值就是 nil 。換句話說,當我們只聲明該類型的變量但沒有用 make 函數對它進行初始化時,該變量的值就會是 nil。我們一定不要忘記初始化通道!
package mainfunc main() {// 示例1。ch1 := make(chan int, 1)ch1 <- 1//ch1 <- 2 // 通道已滿,因此這里會造成阻塞。// 示例2。ch2 := make(chan int, 1)//elem, ok := <-ch2 // 通道已空,因此這里會造成阻塞。//_, _ = elem, okch2 <- 1// 示例3。var ch3 chan int//ch3 <- 1 // 通道的值為nil,因此這里會造成永久的阻塞!//<-ch3 // 通道的值為nil,因此這里會造成永久的阻塞!_ = ch3
}
9. 發送操作和接收操作在什么時候會引發 panic?
對于一個已初始化,但并未關閉的通道來說,收發操作一定不會引發 panic。
但是通道一旦關閉,再對它進行發送操作,就會引發 panic。
另外,如果我們試圖關閉一個已經關閉了的通道,也會引發 panic。
注意,接收操作是可以感知到通道的關閉的,并能夠安全退出。更具體地說,當我們把接收表達式的結果同時賦給兩個變量時,第二個變量的類型就是一定 bool 類型。它的值如果為 false 就說明通道已經關閉,并且再沒有元素值可取了。
注意,如果通道關閉時,里面還有元素值未被取出,那么接收表達式的第一個結果,仍會是通道中的某一個元素值,而第二個結果值一定會是 true 。因此,通過接收表達式的第二個結果值,來判斷通道是否關閉是可能有延時的。
由于通道的收發操作有上述特性,所以除非有特殊的保障措施,我們千萬不要讓接收方關閉通道,而應當讓發送方做這件事。
總結
以上是生活随笔為你收集整理的Go 学习笔记(24)— 并发(03)[通道特点、通道声明、通道发送/接收/关闭、单向通道]的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 安装倒车影像多少钱啊?
- 下一篇: 乌镇二月份穿什么衣服合适