goroutine 與 channel

Go 的並行不是把 thread 包一層糖。Effective Go 濃縮成一句口號:「Do not communicate by sharing memory; instead, share memory by communicating.」這篇講清楚這句話,以及它什麼時候不適用。

goroutine 很便宜

goroutine 不是 OS thread,是 runtime 排程的使用者級協程。初始 stack 約 2KB,按需成長收縮,所以同時開幾十萬個是常態。go f() 就起一個:

go handle(conn) // 一個連線一個 goroutine,不用自己管 thread pool

但便宜不等於免費。goroutine 漏掉(沒有結束條件)會一直佔記憶體,這是 Go 最常見的 leak。每個長命 goroutine 都該有出口:context 取消或 channel 關閉。

channel 與 select

channel 是 goroutine 之間的型別化管道,自帶同步語意。unbuffered 的送與收必須會合;buffered 的滿了才阻塞:

ch := make(chan int)     // unbuffered:送收必須會合
ch := make(chan int, 64) // buffered:滿 64 才擋

select 同時等多個 channel,哪個 ready 走哪個;配 ctx.Done() 做取消、配 time.After 做逾時:

select {
case v := <-in:
    use(v)
case <-ctx.Done():
    return ctx.Err() // 取消就收手
}

用通訊取代共享:actor 心法

讓一個 goroutine 獨佔那塊狀態,其他人用 channel 送訊息進去,不需要多個 goroutine 搶同一把鎖。狀態只被一個 goroutine 摸,就不需要鎖。這就是 Go 版的 actor。

gortex 的 WebSocket Hub 正是這樣:一個 Hub.Run() goroutine 擁有整個 clients map,所有註冊、退出、廣播都經 channel 進到這個唯一的 goroutine(B6〈WebSocket Hub〉細講,gortex-websocket-actor-hub)。

什麼時候別用 channel

channel 不是萬靈丹。Effective Go 自己就說:如果你要的只是「保護一個變數,讓它一次只有一個 goroutine 存取」,用 mutex 把那個計數器包起來,往往更簡單直接。

純計數、簡單旗標、保護一小塊狀態 → sync.Mutexatomic 更快也更好懂。channel 真正適合的是「傳遞所有權、事件流、跨 goroutine 協調」,不是每個共享都要套。這條伏筆扣回框架篇:gortex 的 metrics 計數器用 atomic 加分片鎖(B5),WebSocket Hub 用 channel(B6),同一個專案,兩種工具按場景挑。

Effective Go 精選:Concurrency

Effective Go 的 Concurrency 開頭就是那句「用通訊來共享記憶體,而不是用共享記憶體來通訊」,但它同一段也補了平衡的話:channel 不是要取代 mutex,有時候用一把鎖保護小狀態反而更貼切。重點是 CSP 的精神:用通訊協調並行,而不是反射性地到處上鎖;至於計數這種小事,挑最簡單、最清楚的那個就好。

重點整理

  • goroutine 是便宜的使用者級協程(初始約 2KB),但會 leak,每個都要有結束條件。
  • channel 帶同步語意;select 多路等待,配 ctx.Done() / time.After 收尾。
  • actor 心法:讓一個 goroutine 獨佔狀態,別人用 channel 送訊息,免鎖。
  • 別迷信 channel:純計數、保護小狀態用 mutex / atomic 更簡單(伏筆 → B5 vs B6)。
  • 口訣是「用通訊共享記憶體」,但該用鎖時就用鎖。

延伸:本系列接著用這些觀念拆 gortex 框架,原始碼見 yshengliao/gortex


大綱 Sheng,內文 Claude 協助 · 環境 Go 1.24(當時最新 release)· 本系列為事後回填整理 · 列入 20260613 blog 翻新計劃,新漆未乾。