Golang 並發編程:使用 go 關鍵字啟動 Goroutine
簡介
在現代的伺服器與雲端應用中,高效的併發處理往往是性能瓶頸的關鍵。Go 語言自帶的 Goroutine 讓開發者能以極低的成本,同時執行上千甚至上萬個輕量級執行緒。只需要在函式呼叫前加上 go 關鍵字,即可把工作交給 Go 調度器,讓它自動在背景執行。
對於剛踏入 Go 世界的初學者,了解 「如何正確啟動與管理 Goroutine」 是學習併發編程的第一步;對於已有一定基礎的開發者,掌握常見陷阱與最佳實踐則能避免因競爭條件、資源泄漏等問題導致的隱蔽 bug。本文將從概念說明、實作範例、常見問題到實務應用,完整介紹 go 關鍵字的使用方式,幫助你在真實專案中安全、有效地運用 Goroutine。
核心概念
1. Goroutine 是什麼?
- Goroutine 是由 Go 執行時(runtime)管理的輕量級執行緒。
- 每個 Goroutine 只佔用約 2KB 的堆疊空間,且會根據需要自動伸縮。
- 調度器會把多個 Goroutine 映射到少數 OS 執行緒上,實現「M:N」模型,提升 CPU 利用率。
2. go 關鍵字的語法
go functionName(arg1, arg2, …)
go必須緊貼在 函式呼叫 前,不能單獨使用。- 被呼叫的函式會在新的 Goroutine中執行,原本的執行流程則立即繼續往下走。
- 若函式返回值,無法直接取得;若需要結果,必須透過 channel、sync.WaitGroup 或其他同步機制傳遞。
3. 為什麼要使用 Goroutine?
| 場景 | 傳統做法 | 使用 Goroutine 的好處 |
|---|---|---|
| I/O 密集 (HTTP 請求、資料庫查詢) | 阻塞式呼叫,等待回應 | 同時發起多個請求,縮短總等待時間 |
| CPU 密集 (圖像處理、加密) | 多執行緒手動管理 | 調度器自動平衡工作負載,減少鎖競爭 |
| 定時任務或背景工作 | 另建服務或使用 cron | 輕鬆在程式內部啟動,資源占用低 |
程式碼範例
以下示範 5 個常見且實用的 go 用法,從最簡單的印出訊息,到結合 channel 與 sync.WaitGroup 的完整範例。
1️⃣ 基礎範例:印出訊息
package main
import (
"fmt"
"time"
)
func hello() {
fmt.Println("Hello from Goroutine!")
}
func main() {
go hello() // <-- 啟動 Goroutine
fmt.Println("Main function")
time.Sleep(time.Second) // 等待 Goroutine 完成(僅示範用)
}
說明:
go hello()立即返回,main仍會繼續執行。若沒有time.Sleep,程式可能在 Goroutine 完成前就結束。
2️⃣ 傳遞參數與匿名函式
package main
import "fmt"
func main() {
for i := 1; i <= 3; i++ {
// 使用匿名函式捕獲 i 的當前值
go func(n int) {
fmt.Printf("Goroutine %d 執行中\n", n)
}(i)
}
// 讓所有 Goroutine 有機會執行
select {}
}
重點:若直接在迴圈內使用
go func(){ fmt.Println(i) }(),所有 Goroutine 可能會印出同一個(最終)值。使用參數傳遞可避免此問題。
3️⃣ 使用 sync.WaitGroup 等待多個 Goroutine 完成
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 完成時呼叫 Done
fmt.Printf("Worker %d 開始\n", id)
time.Sleep(time.Duration(id) * time.Second)
fmt.Printf("Worker %d 完成\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 註冊一個待完成的工作
go worker(i, &wg) // 啟動 Goroutine
}
wg.Wait() // 等待全部 Done
fmt.Println("所有工作已完成")
}
說明:
WaitGroup是同步多個 Goroutine 的常用工具,避免使用time.Sleep造成不確定的等待時間。
4️⃣ 透過 Channel 傳遞結果
package main
import (
"fmt"
"math/rand"
"time"
)
func randomGenerator(ch chan<- int) {
defer close(ch) // 完成後關閉 channel
for i := 0; i < 5; i++ {
ch <- rand.Intn(100) // 將產生的隨機數傳出去
time.Sleep(200 * time.Millisecond)
}
}
func main() {
rand.Seed(time.Now().UnixNano())
ch := make(chan int)
go randomGenerator(ch) // 啟動 Goroutine
for v := range ch { // 讀取直到 channel 被關閉
fmt.Println("收到:", v)
}
fmt.Println("產生完畢")
}
重點:使用 單向 channel (
chan<-) 明確表達「只寫」的意圖,提高程式可讀性與安全性。
5️⃣ 併發 HTTP 請求(實務案例)
package main
import (
"fmt"
"io/ioutil"
"net/http"
"sync"
)
func fetch(url string, wg *sync.WaitGroup, ch chan<- string) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprintf("%s => error: %v", url, err)
return
}
body, _ := ioutil.ReadAll(resp.Body)
resp.Body.Close()
ch <- fmt.Sprintf("%s => %d bytes", url, len(body))
}
func main() {
urls := []string{
"https://golang.org",
"https://www.google.com",
"https://www.github.com",
}
var wg sync.WaitGroup
resultCh := make(chan string, len(urls))
for _, u := range urls {
wg.Add(1)
go fetch(u, &wg, resultCh) // 每個 URL 用一個 Goroutine
}
wg.Wait()
close(resultCh)
for r := range resultCh {
fmt.Println(r)
}
}
說明:此範例展示如何同時發起多個 HTTP 請求,使用
WaitGroup確保全部完成,最後透過緩衝 channel 收集結果。這正是微服務或爬蟲系統常見的併發模式。
常見陷阱與最佳實踐
| 陷阱 | 說明 | 解決方案 |
|---|---|---|
| Goroutine 泄漏 | Goroutine 仍在等待 channel、mutex 等資源,卻永遠不會被喚醒,導致程式無法結束。 | 使用 context.Context 取消、在 select 中加入超時或 done channel。 |
| 競爭條件 (Race Condition) | 多個 Goroutine 同時讀寫同一變數,結果不可預期。 | 使用 sync.Mutex、sync.RWMutex 或 channel 進行同步。 |
忘記 WaitGroup.Done() |
wg.Wait() 會永遠阻塞,程式卡死。 |
在 Goroutine 開頭使用 defer wg.Done(),確保一定會呼叫。 |
| 捕獲迴圈變數 | 在迴圈內直接啟動 Goroutine,所有 Goroutine 可能看到同一個最終值。 | 透過參數傳遞或在匿名函式內重新宣告變數。 |
| 過度產生 Goroutine | 短時間內產生大量 Goroutine 會耗盡記憶體或調度器資源。 | 使用 worker pool(工作池)或限制同時執行的 Goroutine 數量。 |
不當使用 select |
忽略 default 會造成阻塞,或在 default 中執行耗時操作。 |
只在需要非阻塞檢查時使用 default,耗時工作仍放在 Goroutine 中。 |
最佳實踐
- 盡量使用 channel 進行資料傳遞,讓同步行為更具表意。
- 為長時間執行的 Goroutine 加入取消機制(
context.WithCancel、WithTimeout),避免資源泄漏。 - 使用
sync.WaitGroup或errgroup.Group來統一管理多個 Goroutine 的生命週期。 - 在測試時加入
-race標誌(go test -race),即時偵測競爭條件。 - 限制同時執行的 Goroutine 數量,例如使用緩衝 channel 作為 semaphore。
實際應用場景
高併發 API 伺服器
- 每個 HTTP 請求在
net/http包內部已經以 Goroutine 處理,開發者只需在 handler 中再啟動子 Goroutine 處理 I/O 密集的外部服務呼叫,提升整體吞吐量。
- 每個 HTTP 請求在
分散式爬蟲
- 透過 Goroutine 同時抓取多個網頁,利用 channel 收集結果,再寫入資料庫或消息佇列。配合
context可在爬取超時或手動停止時快速回收資源。
- 透過 Goroutine 同時抓取多個網頁,利用 channel 收集結果,再寫入資料庫或消息佇列。配合
即時資料流處理
- 使用多條 pipeline(每條 pipeline 為一組 Goroutine)對 Kafka、RabbitMQ 等訊息來源進行並行解析、過濾、寫入,實現低延遲的資料處理系統。
背景任務排程
- 在服務啟動時啟動定時 Goroutine(
time.Ticker),執行資料備份、緩存刷新或統計報表產生等工作,避免額外的外部排程系統。
- 在服務啟動時啟動定時 Goroutine(
微服務間的並行呼叫
- 當單一 API 需要同時向多個微服務請求資料時,可在同一個 handler 中啟動多個 Goroutine,最後彙總結果回傳,顯著降低端到端的回應時間。
總結
go關鍵字是啟動 Goroutine 的唯一且簡潔的方式,只要在函式呼叫前加上go,即可把工作交給 Go 調度器在背景執行。- 透過 channel、sync.WaitGroup、context 等同步工具,我們可以安全地取得結果、控制生命週期,並避免常見的競爭條件與資源泄漏。
- 在實務開發中,適時使用 Goroutine 能大幅提升 I/O 密集或 CPU 密集任務的併發效能;同時,遵守最佳實踐(限制 Goroutine 數量、加入取消機制、使用
-race檢測)可確保系統的穩定與可維護性。
掌握了 go 關鍵字的正確使用方式後,你就能在 Go 生態系統中自如地構建高效、可擴展的併發應用,為未來的微服務、即時系統與大數據處理奠定堅實基礎。祝你在 Golang 的併發世界裡玩得開心、寫出乾淨的程式碼!