瞭解channel
概念:傳送帶 / 管道
你可以把 Channel(通道) 想象成一條在協程(Goroutine) 之間傳送數據的傳送帶或者管道。
- 協程(Goroutine):就像工廠裏的工人。
- Channel(通道):就像連接兩個工人工作台的傳送帶。
Channel 的主要作用
- 通信(Communication)
- 這是最基本的作用。一個協程把數據(比如一個零件)放在傳送帶的一端(發送),另一個協程從傳送帶的另一端取走這個數據(接收)。數據就成功地從 A 協程傳遞到了 B 協程。
- 同步(Synchronization)
- 這是 Channel 一個極其重要的“副作用”。它保證了協程之間的執行順序。
- 沒有緩衝區的 Channel(Unbuffered Channel) 就像一次只能傳一個零件的、手遞手的傳送帶。
- 發送者把零件放上傳送帶後,必須等待接收者把它取走,才能繼續幹下一件事(發送下一個數據)。
- 接收者在零件到達之前,必須等待發送者把零件放上來。
- 這個過程強制了兩個協程在同一個時間點“碰頭”,完成了同步。
- 防止競態條件(Preventing Race Conditions)
- 多個工人(協程)如果同時去操作一個共享的工具箱(共享變量),可能會發生爭搶,導致數據錯亂。這被稱為“競態條件”。
- 通過 Channel,我們可以把“操作共享數據”這個任務,變成一個“通過傳送帶傳遞任務”的模型。比如,我們指定只有一個特殊的“管理員”協程可以操作工具箱,其他協程如果需要工具,就把請求(數據)通過 Channel 發給管理員,然後等待管理員通過另一個 Channel 把工具(結果)送回來。這樣就避免了多個協程直接衝突。
生動的場景舉例
假設我們有一個主線程(廠長)和兩個協程(工人A和工人B)。
沒有 Channel 的世界(混亂):
- 廠長説:“A去生產零件,B去組裝。”
- A和B同時跑向倉庫拿原料,可能會撞在一起(競態條件)。
- B可能跑得太快,在A還沒生產出零件時,就開始組裝空氣(數據不同步)。
有 Channel 的世界(井然有序):
場景一:簡單的通信與同步(使用無緩衝Channel)
// 創建一個傳送帶(無緩衝Channel)
ch := make(chan string)
// 工人A(協程)
go func() {
result := "零件A做好了" // 1. 工人A生產零件
ch <- result // 2. 把零件放上傳送帶。此時如果沒人來接,A就等着(阻塞)
fmt.Println("A:我把零件交出去了")
}()
// 主線程(廠長)
message := <-ch // 3. 廠長從傳送帶取下零件。如果零件沒到,廠長就等着(阻塞)
fmt.Println("主線程收到了:", message)
// 輸出:
// 主線程收到了: 零件A做好了
// A:我把零件交出去了
看,因為 Channel 的同步特性,A:我把零件交出去了 這句話總是在零件被主線程接收之後才打印。
場景二:帶緩衝的通信(像一個小型倉庫)
// 創建一個能存放2個零件的傳送帶(緩衝為2的Channel)
ch := make(chan string, 2)
// 工人A可以連續放兩個零件,而不用馬上等別人來取
ch <- "零件1"
ch <- "零件2"
// ch <- "零件3" // 如果此時再放第三個,因為倉庫滿了,工人A就會阻塞等待
// 工人B可以連續取走兩個零件
fmt.Println(<-ch) // 零件1
fmt.Println(<-ch) // 零件2
這種 Channel 更側重於通信的吞吐量,而不是強同步。
總結
所以,通俗地講,Channel 的作用就是:
它為協程們提供了一條安全、有序的“數據傳輸管道”。不僅解決了“怎麼傳”的通信問題,更重要的是通過“等待”機制,巧妙地解決了“什麼時候傳”的同步問題,從而讓併發編程變得簡單和安全。
channel的基本定義與使用
定義
make(chan Type) //無緩衝 等價於make(chan Type,0)
make(chan Type, capacity) //有緩衝
channel <- value //發送value到channel
<- channel //接收並將其丟棄
x := <- channel //從channel中接收數據,並賦值給x
x, ok := <- channel //功能同上,同時檢查通道是否已關閉或者是否為空
使用
import (
"fmt"
)
func main() {
//定義一個channel
c := make(chan int)
//啓動一個goroutine,向channel中發送數據
go func() {
defer fmt.Println("goroutine end")
fmt.Println("goroutine start to send data")
c <- 666 //向channel中發送數據666
}()
num := <-c //從channel中接收數據並賦值給num
fmt.Println("main received data:", num)
fmt.Println("main goroutine end")
}
---------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
goroutine start to send data
goroutine end
main received data: 666
main goroutine end
channel的有緩衝和無緩衝
無緩衝
- 第1步,兩個 goroutine 都到達通道,但哪個都沒有開始執行發送或者接收。
- 第 2步,左側的 goroutine 將它的手伸進了通道,這模擬了向通道發送數據的行為。這時,這個 goroutine 會在通道中被鎖住,直到交換完成。
- 第 3 步,右側的 goroutine 將它的手放入通道,這模擬了從通道里接收數據。這個 goroutine 一樣也會在通道中被鎖住,直到交換完成。
- 第 4步和第 5 步,進行交換,並最終,在第6步,兩個goroutine 都將它們的手從通道里拿出來,這模擬了被鎖住的 goroutine 得到釋放。兩個 goroutine 現在都可以去做其他事情了。
有緩衝
- 第1步,右側的 goroutine 正在從通道接收一個值。
- 第 2步,右側的這個 goroutine獨立完成了接收值的動作,而左側的goroutine 正在發送一個新值到通道里。
- 第 3 步,左側的goroutine 還在向通道發送新值,而右側的 goroutine 正在從通道接收另外一個值。這個步驟裏的兩個操作既不是同步的,也不會互相阻塞。
- 最後,第4步,所有的發送和接收都完成,而通道里還有幾個值,也有一些空間可以存更多的值。
- 發生阻塞的情況:如果左側放滿了右側還沒取左側會阻塞、或者右側取空了左側還沒放右側會阻塞
在上述定義中的實例是無緩衝channel,下面做有緩衝channel的示範
package main
import (
"fmt"
"time"
)
func main() {
//定義一個有緩衝channel
c := make(chan int, 3)
fmt.Println("len(c) = ", len(c), ",cap(c) = ", cap(c))
//啓動一個goroutine,向channel中發送數據
go func() {
defer fmt.Println("goroutine end")
for i := 0; i < 3; i++ {
c <- i //向channel中發送數據
fmt.Println("goroutine start to send data:", i, "len(c)=", len(c), ",cap(c)=", cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 3; i++ {
num := <-c //從channel中接收數據
fmt.Println("main goroutine receive data:", num, "len(c)=", len(c), ",cap(c)=", cap(c))
}
fmt.Println("main goroutine end")
}
---------------------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
len(c) = 0 ,cap(c) = 3
goroutine start to send data: 0 len(c)= 1 ,cap(c)= 3
goroutine start to send data: 1 len(c)= 2 ,cap(c)= 3
goroutine start to send data: 2 len(c)= 3 ,cap(c)= 3
goroutine end
main goroutine receive data: 0 len(c)= 2 ,cap(c)= 3
main goroutine receive data: 1 len(c)= 1 ,cap(c)= 3
main goroutine receive data: 2 len(c)= 0 ,cap(c)= 3
main goroutine end
上述可以看到協程發送完數據結束,主線程接收完數據結束。
如果發生或接收數據超過channel容量:
func main() {
//定義一個有緩衝channel
c := make(chan int, 3)
fmt.Println("len(c) = ", len(c), ",cap(c) = ", cap(c))
//啓動一個goroutine,向channel中發送數據
go func() {
defer fmt.Println("goroutine end")
for i := 0; i < 4; i++ {
c <- i //向channel中發送數據
fmt.Println("goroutine start to send data:", i, "len(c)=", len(c), ",cap(c)=", cap(c))
}
}()
time.Sleep(2 * time.Second)
for i := 0; i < 4; i++ {
num := <-c //從channel中接收數據
fmt.Println("main goroutine receive data:", num, "len(c)=", len(c), ",cap(c)=", cap(c))
}
fmt.Println("main goroutine end")
}
--------------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
len(c) = 0 ,cap(c) = 3
goroutine start to send data: 0 len(c)= 1 ,cap(c)= 3
goroutine start to send data: 1 len(c)= 2 ,cap(c)= 3
goroutine start to send data: 2 len(c)= 3 ,cap(c)= 3
main goroutine receive data: 0 len(c)= 3 ,cap(c)= 3
main goroutine receive data: 1 len(c)= 2 ,cap(c)= 3
main goroutine receive data: 2 len(c)= 1 ,cap(c)= 3
main goroutine receive data: 3 len(c)= 0 ,cap(c)= 3
main goroutine end
超過容量協程繼續發送數據就會阻塞協程不會提前結束,而主線程已經結束了。(輸出也會有不同的情況,不過相同的情況是協程會發生阻塞不會提前結束)
關閉channel
通過close()來關閉channel
正常關閉
package main
import (
"fmt"
)
func main() {
//定義一個channel
c := make(chan int)
//啓動一個goroutine,向channel中發送數據
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以關閉channel
close(c)
}()
for {
//ok如果為true,表示channel沒有關閉,如果為false表示channel已經關閉
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main goroutine end")
}
---------------------------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
0
1
2
3
4
main goroutine end
沒有關閉
沒有關閉會報死鎖的錯誤,因為協程中已經向channel發送完畢數據了,主線程中依然還在等待數據導致主函數阻塞。(對應緩衝中提到過的阻塞情況)
func main() {
//定義一個channel
c := make(chan int)
//啓動一個goroutine,向channel中發送數據
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以關閉channel
//close(c)
}()
for {
//ok如果為true,表示channel沒有關閉,如果為false表示channel已經關閉
if data, ok := <-c; ok {
fmt.Println(data)
} else {
break
}
}
fmt.Println("main goroutine end")
}
---------------------------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
0
1
2
3
4
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan receive]:
main.main()
D:/GoProject/firstGoProject/firstGoProject.go:23 +0xbd
exit status 2
注:
- channel不像文件一樣需要經常去關閉,只有當你確實沒有任何發送數據了,或者你想顯式的結束range循環之類的,才去關閉channel;
- 關閉channel後,無法向channel 再發送數據(引發 panic 錯誤後導致接收立即返回零值);
- 關閉channel後,可以繼續從channel接收數據;
- 對於nil channel,無論收發都會被阻塞。
channel與range
func main() {
//定義一個channel
c := make(chan int)
//啓動一個goroutine,向channel中發送數據
go func() {
for i := 0; i < 5; i++ {
c <- i
}
//close可以關閉channel
close(c)
}()
// for {
// //ok如果為true,表示channel沒有關閉,如果為false表示channel已經關閉
// if data, ok := <-c; ok {
// fmt.Println(data)
// } else {
// break
// }
// }
//使用for range遍歷channel,效果與註釋掉的代碼相同
for data := range c {
fmt.Println(data)
}
fmt.Println("main goroutine end")
}
----------------------------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
0
1
2
3
4
main goroutine end
channel與select
單流程下一個go只能監控一個channel的狀態,select可以完成監控多個channel的狀態。
package main
import (
"fmt"
)
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
//如果c可寫,則case就會進來
x = y
y = x + y
case <-quit:
//如果quit可讀,則case就會進來
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
//sub go
go func() {
for i := 0; i < 6; i++ {
fmt.Println(<-c)
}
quit <- 0 //通知子go退出
}()
//main go
fibonacci(c, quit)
}
---------------------------------------------------------------------
PS D:\GoProject\firstGoProject> go run firstGoProject.go
1
1
2
4
8
16
quit
select {
case <- chanl:
//如果chan1成功讀到數據,則進行該case處理語句
case chan2 <- 1:
//如果成功向chan2寫入數據,則進行該case處理語句
default:
//如果上面都沒有成功,則進入defau1t處理流程
}