瞭解channel

概念:傳送帶 / 管道

你可以把 Channel(通道) 想象成一條在協程(Goroutine) 之間傳送數據的傳送帶或者管道

  • 協程(Goroutine):就像工廠裏的工人。
  • Channel(通道):就像連接兩個工人工作台的傳送帶。

Channel 的主要作用

  1. 通信(Communication)
  • 這是最基本的作用。一個協程把數據(比如一個零件)放在傳送帶的一端(發送),另一個協程從傳送帶的另一端取走這個數據(接收)。數據就成功地從 A 協程傳遞到了 B 協程。
  1. 同步(Synchronization)
  • 這是 Channel 一個極其重要的“副作用”。它保證了協程之間的執行順序。
  • 沒有緩衝區的 Channel(Unbuffered Channel) 就像一次只能傳一個零件的、手遞手的傳送帶。
  • 發送者把零件放上傳送帶後,必須等待接收者把它取走,才能繼續幹下一件事(發送下一個數據)。
  • 接收者在零件到達之前,必須等待發送者把零件放上來。
  • 這個過程強制了兩個協程在同一個時間點“碰頭”,完成了同步。
  1. 防止競態條件(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的有緩衝和無緩衝

無緩衝

Golang channel 用法簡介 - Go語言中文網_主線程

  • 第1步,兩個 goroutine 都到達通道,但哪個都沒有開始執行發送或者接收。
  • 第 2步,左側的 goroutine 將它的手伸進了通道,這模擬了向通道發送數據的行為。這時,這個 goroutine 會在通道中被鎖住,直到交換完成。
  • 第 3 步,右側的 goroutine 將它的手放入通道,這模擬了從通道里接收數據。這個 goroutine 一樣也會在通道中被鎖住,直到交換完成。
  • 第 4步和第 5 步,進行交換,並最終,在第6步,兩個goroutine 都將它們的手從通道里拿出來,這模擬了被鎖住的 goroutine 得到釋放。兩個 goroutine 現在都可以去做其他事情了。
有緩衝

Golang channel 用法簡介 - Go語言中文網_主線程_02

  • 第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處理流程
}