Day 5: 無緩衝的同步之舞 vs. 有緩衝的非同步效率
前言
在 Day 4,我們初次體驗了 Channel 的魔力。它不僅能在 Goroutine 之間安全地傳遞資料,其阻塞的特性還為我們提供了「免費」的同步。我們昨天使用的 make(chan T) 建立的,是 無緩衝 Channel (Unbuffered Channel)。
然而,Channel 的世界並非只有一種形態。今天,我們要深入探討 Channel 的另一種樣貌——有緩衝 Channel (Buffered Channel),並比較這兩者在不同場景下的優劣。這就像選擇交通工具,有時你需要的是能精準同步的F1賽車(無緩衝),有時你需要的則是可以暫存大量貨物的卡車(有緩衝)。
一個比喻:遞交報告
讓我們再次回到辦公室的場景。
情境一:無緩衝 Channel (面對面遞交)
你寫完一份報告,需要親手交給你的經理。你走到經理的辦公室,但經理正在開會。你該怎麼辦?你只能站在門口一直等,直到經理開完會,伸出手來接過你的報告,你才能離開去做別的事。
這就是無緩衝 Channel。發送方 (你) 和接收方 (經理) 必須同時在場,才能完成一次資料交換。這個交換的瞬間,就是一個同步點。
情境二:有緩衝 Channel (經理桌上的文件匣)
你的經理非常忙,所以他在桌上放了一個文件匣,並規定:「報告放在這裡就好,但最多只能放 3 份,滿了就別放了」。
現在,你寫完報告,你只需要走到經理辦公室,把報告放進文件匣,然後立刻就可以轉身離開,去做下一件事。你根本不在乎經理什麼時候會去看。只有一種情況你會等待:當文件匣已經滿了(放了 3 份報告),你才必須等待,直到經理從裡面拿走一份報告,空出位置。
這就是有緩衝 Channel。它提供了一個緩衝區(文件匣),在緩衝區未滿的情況下,發送方可以非同步地發送資料,無需等待接收方。
無緩衝 Channel:強同步的保證
我們昨天已經見識過它了。它的建立方式是:
// 容量為 0,這就是無緩衝 Channel
ch := make(chan int)
核心特性:發送操作會阻塞,直到接收方準備好接收。
這會導致一個非常常見的死鎖 (Deadlock) 場景:
package main
import "fmt"
func main() {
	ch := make(chan int)
	fmt.Println("Sending 42 to channel...")
	ch <- 42 // 程式會永遠卡在這裡!
	fmt.Println("Send operation finished.") // 這行永遠不會被執行
}
執行這段程式碼,你會得到一個致命錯誤:
Sending 42 to channel...
fatal error: all goroutines are asleep - deadlock!
為什麼死鎖?
因為 main Goroutine 嘗試向 ch 發送 42,但此刻沒有任何其他 Goroutine 準備從 ch 接收。發送操作被永久阻塞了,而程式中又沒有其他可運行的 Goroutine,Go Runtime 檢測到這種無解的等待,於是觸發了 deadlock 恐慌。
使用時機:當你需要一個強烈的信號,確保發送的資料確實被另一方接收時,無緩衝 Channel 是絕佳選擇。發送成功本身,就意味著一次成功的「交接」。
有緩衝 Channel:提升吞吐量的利器
有緩衝 Channel 在建立時,需要額外提供一個容量 (capacity) 參數。
// 建立一個容量為 3 的 int 型別 channel
ch := make(chan int, 3)
核心特性:
- 當緩衝區未滿時,發送操作不會阻塞。
 - 當緩衝區已滿時,發送操作會阻塞,直到有接收方從 Channel 取走資料。
 - 當緩衝區為空時,接收操作會阻塞。
 
讓我們用程式碼來模擬文件匣的例子:
package main
import "fmt"
func main() {
	// 經理的文件匣,容量為 3
	reportBox := make(chan string, 3)
	// 員工提交了 3 份報告,這些操作都是非阻塞的
	reportBox <- "Report 1"
	fmt.Println("Sent Report 1")
	reportBox <- "Report 2"
	fmt.Println("Sent Report 2")
	reportBox <- "Report 3"
	fmt.Println("Sent Report 3")
	// 此時文件匣已滿。如果再嘗試發送,程式就會阻塞
	// reportBox <- "Report 4" // 取消這行的註解會導致 deadlock
	// 經理現在開始讀取報告
	fmt.Println("Manager is reading:", <-reportBox)
	fmt.Println("Manager is reading:", <-reportBox)
	fmt.Println("Manager is reading:", <-reportBox)
}
執行結果:
Sent Report 1
Sent Report 2
Sent Report 3
Manager is reading: Report 1
Manager is reading: Report 2
Manager is reading: Report 3
使用時機:當你希望解耦生產者和消費者時。例如,你有一個 Goroutine 快速地產生大量任務,而另一組 Goroutine (Worker) 以它們自己的速度來處理這些任務。有緩衝 Channel 可以作為任務佇列,有效提升系統的整體吞吐量。
總結與對比
| 特性 | 無緩衝 (Unbuffered) Channel | 有緩衝 (Buffered) Channel | 
|---|---|---|
| 建立 | make(chan T) | make(chan T, capacity) | 
| 容量 | 0 | > 0 | 
| 發送行為 | 總是阻塞,直到接收方就緒 | 緩衝區未滿時不阻塞,滿了才阻塞 | 
| 接收行為 | 總是阻塞,直到發送方就緒 | 緩衝區為空時阻塞 | 
| 核心用途 | 強同步、信號傳遞 | 解耦、非同步、提升吞吐量 | 
今日總結
今天,我們深入剖析了兩種 Channel 的內在差異:
- 無緩衝 Channel 是一次同步的握手,保證了訊息的即時交付。
 - 有緩衝 Channel 是一個非同步的佇列,允許生產者和消費者以不同的速率工作,起到了削峰填谷的作用。
 
選擇哪種 Channel 並沒有絕對的對錯,完全取決於你的業務場景。你需要的是強同步保證,還是更高的系統吞吐量?理解它們的根本差異,是寫出高效、健壯的 Go 併發程式的關鍵一步。
到目前為止,我們都是手動地、一次一次地從 Channel 接收資料。如果一個 Goroutine 會持續不斷地產生資料,直到它工作完成為止,接收方該如何優雅地接收所有資料,並在發送方停止時自動停止呢?
預告 Day 6: 【優雅地接收】用for...range遍歷Channel,直到世界盡頭。我們將學習如何關閉 Channel 以及如何使用 for...range 迴圈來優雅地消費 Channel 中的所有資料。