4.1 goroutine:輕量並發工作
goroutine 是 Go 執行並發工作的基本單位。它的核心用途是讓一段函式和目前流程同時進行,但每個 goroutine 都必須有明確的退出條件,否則長時間程式會累積無法回收的背景工作。
本章目標
學完本章後,你將能夠:
- 用
go啟動 goroutine - 理解 goroutine 和一般函式呼叫的差異
- 判斷哪些工作適合放進 goroutine
- 為 goroutine 設計退出條件
- 避免 goroutine leak
【觀察】go 關鍵字啟動並發工作
go 的核心規則是:在函式呼叫前加上 go,該函式會在新的 goroutine 中執行,呼叫端不會等待它完成。
1func say(message string) {
2 fmt.Println(message)
3}
4
5func main() {
6 go say("background")
7 say("foreground")
8}這段程式啟動一個背景 goroutine 執行 say("background"),主 goroutine 會繼續執行 say("foreground")。
【判讀】goroutine 需要明確完成保證
goroutine 的生命週期規則是:程式不會因為你啟動了 goroutine 就自動等待它完成。main() 結束時,整個 process 會結束,尚未完成的 goroutine 也會停止。
因此,這段程式可能看不到背景輸出:
1func main() {
2 go fmt.Println("background")
3}主程式太快結束時,背景 goroutine 可能還沒得到執行機會。
需要等待結果時,應該使用 channel、sync.WaitGroup 或其他同步機制。
【策略】goroutine 適合等待型或獨立型工作
goroutine 使用的核心規則是:只有當工作能和目前流程並發進行,且生命週期可被管理時,才啟動 goroutine。
適合 goroutine 的工作:
| 工作類型 | 原因 |
|---|---|
| 等待 I/O | 等檔案、網路、外部程序時不阻塞主流程 |
| 背景 worker | 從 channel 收 job 並處理 |
| 定時任務 | 定期清理、同步或掃描 |
| 多個獨立請求 | 可同時發出、再收集結果 |
等待 I/O 的核心訊號是目前流程會花時間等外部回應,例如讀檔、呼叫 HTTP API、等待資料庫查詢或讀取 socket。這類工作放進 goroutine 後,呼叫端可以繼續處理其他事件,但仍然要用 context 或 channel 管理結果與取消。
背景 worker 的核心訊號是工作來自 queue 或 channel,而且處理時間和 request 生命週期分離。例如使用者送出匯入任務後,server 只先接受任務,後續由 worker 逐筆處理資料。這種 goroutine 通常需要明確的 job channel、錯誤回報與 shutdown 流程。
定時任務的核心訊號是行為按時間觸發,例如每分鐘清理過期 session、同步外部狀態或刷新快取。這類 goroutine 應使用 ticker 搭配 context,讓服務停止時可以一起退出。
多個獨立請求的核心訊號是多個工作彼此沒有順序依賴,例如同時查三個外部 API,最後合併結果。這類 goroutine 的重點是收集結果、限制並發數量,並在其中一個工作失敗時決定是否取消其他工作。
需要先補齊生命週期設計的情境:
- 只是想讓程式「看起來比較快」
- 沒有任何退出條件
- 呼叫端需要結果但沒有同步機制
- 多個 goroutine 會同時修改共享資料但沒有保護
【執行】用 WaitGroup 等待一組工作
sync.WaitGroup 的核心用途是等待一組 goroutine 完成。
1func main() {
2 var wg sync.WaitGroup
3
4 for i := 0; i < 3; i++ {
5 wg.Add(1)
6 go func(id int) {
7 defer wg.Done()
8 fmt.Println("worker", id)
9 }(i)
10 }
11
12 wg.Wait()
13}這段程式有三個關鍵:
| 動作 | 意義 |
|---|---|
wg.Add(1) | 增加一個待完成工作 |
defer wg.Done() | goroutine 結束時標記完成 |
wg.Wait() | 等待所有工作完成 |
id 作為參數傳入 goroutine,可以避免 loop 變數捕捉造成混淆。
長時間 goroutine 要能停止
長時間 goroutine 的核心規則是:迴圈中必須等待取消訊號或輸入 channel 關閉。
1func worker(ctx context.Context, jobs <-chan Job) {
2 for {
3 select {
4 case <-ctx.Done():
5 return
6 case job, ok := <-jobs:
7 if !ok {
8 return
9 }
10 handle(job)
11 }
12 }
13}這個 worker 不會無限卡住;上層取消 context 或關閉 jobs channel,它都會退出。
設計檢查
需要結果時要有等待機制
需要結果或完成保證時,goroutine 應搭配 channel 或 WaitGroup。go doWork() 只負責啟動工作,結果收集與完成等待需要另外設計。
錯誤要有回報路徑
goroutine 裡的錯誤需要明確回報路徑。需要錯誤結果時,用 channel 傳回:
1errCh := make(chan error, 1)
2go func() {
3 errCh <- doWork()
4}()
5
6if err := <-errCh; err != nil {
7 return err
8}長時間工作要有退出條件
長時間 worker 至少要監聽 context 或 channel close。永遠 for {} 會讓 goroutine 生命週期失去 owner,服務停止時也難以清理。