目錄
什麼是 Goroutine?
Goroutine 是 Go 語言中的輕量級執行單元,類似於執行緒(thread)但更輕量。
基本特性
- 輕量級:每個 goroutine 只佔用約 2KB 記憶體(執行緒通常需要 1-2MB)
- 高效:一個程式可以輕鬆執行成千上萬個 goroutines
- 內建支援:使用
go關鍵字即可啟動 - 自動管理:由 Go runtime 自動調度,無需手動管理
基本語法
// 啟動一個 goroutine
go functionName()
// 使用匿名函數
go func() {
fmt.Println("在 goroutine 中執行")
}()
// 傳遞參數
go func(msg string) {
fmt.Println(msg)
}("Hello from goroutine")
Goroutine vs Thread(執行緒)
| 特性 | Goroutine | Thread(執行緒) |
|---|---|---|
| 記憶體佔用 | 2KB 起始(可動態增長) | 1-2MB 固定大小 |
| 建立成本 | 極低(微秒級) | 較高(毫秒級) |
| 調度方式 | Go runtime(使用者空間) | OS kernel(核心空間) |
| 切換成本 | 低(只需保存少量暫存器) | 高(需要完整上下文切換) |
| 數量限制 | 可輕鬆達到數十萬個 | 通常數千個就是極限 |
| 管理方式 | 自動管理 | 需要手動管理 |
為什麼 Goroutine 更輕量?
- 動態堆疊:起始只有 2KB,需要時自動增長
- 使用者空間調度:避免昂貴的系統呼叫
- 協作式調度:減少不必要的上下文切換
- 共享記憶體:所有 goroutines 共享同一個位址空間
Go 調度器(GMP 模型)
Go 使用 M:N 調度模型,將 M 個 goroutines 映射到 N 個 OS threads。
GMP 三要素
G (Goroutine) → 輕量級執行單元
M (Machine) → OS 執行緒(Thread)
P (Processor) → 邏輯處理器(調度上下文)
調度模型示意
+-------------------+
| Goroutines (G) | ← 大量輕量級執行單元
+-------------------+
↓
+-------------------+
| Processors (P) | ← 邏輯處理器(通常 = CPU 核心數)
+-------------------+
↓
+-------------------+
| Threads (M) | ← OS 執行緒(少量)
+-------------------+
↓
+-------------------+
| CPU Cores | ← 實體 CPU 核心
+-------------------+
調度流程
- 建立 Goroutine:使用
go關鍵字建立,進入 P 的本地佇列 - P 調度:P 從本地佇列取出 G 並分配給 M 執行
- 執行:M 執行 G 的程式碼
- 阻塞處理:
- 若 G 阻塞(如 I/O),M 會被釋放給其他 G 使用
- 阻塞的 G 會被移到等待佇列
- 工作竊取:若 P 的本地佇列空了,會從其他 P 偷取 G(work stealing)
為什麼使用 M:N 模型?
- 充分利用 CPU:P 的數量通常等於 CPU 核心數
- 避免阻塞:一個 goroutine 阻塞不會影響其他 goroutines
- 負載均衡:work stealing 機制確保所有 P 都有工作
Goroutine 生命週期
1. 建立階段
// main 函數本身就是一個 goroutine
func main() {
// 建立新的 goroutine
go worker()
// main goroutine 繼續執行
fmt.Println("Main goroutine")
}
func worker() {
fmt.Println("Worker goroutine")
}
2. 執行階段
Goroutine 會執行直到:
- 函數執行完畢(正常結束)
- 呼叫
runtime.Goexit()(主動結束) - 程式終止(main goroutine 結束)
3. 結束階段
func main() {
go func() {
fmt.Println("Goroutine 開始")
time.Sleep(1 * time.Second)
fmt.Println("Goroutine 結束")
}()
// 等待 goroutine 完成
time.Sleep(2 * time.Second)
// main 結束時,所有 goroutines 都會被終止
}
重要觀念
⚠️ Main Goroutine 結束 = 程式結束
func main() {
go func() {
time.Sleep(1 * time.Second)
fmt.Println("這行可能不會被執行")
}()
// main 立即結束,上面的 goroutine 來不及執行
}
解決方案:使用 sync.WaitGroup 等待所有 goroutines 完成
Channel 通訊
Goroutines 之間通過 channels 進行通訊,實現 CSP(Communicating Sequential Processes)模型。
基本用法
// 建立 channel
ch := make(chan int)
// 發送資料
go func() {
ch <- 42 // 發送到 channel
}()
// 接收資料
value := <-ch // 從 channel 接收
fmt.Println(value) // 輸出: 42
Channel 類型
1. 無緩衝 Channel(同步)
ch := make(chan int)
// 發送和接收會阻塞,直到雙方都準備好
go func() {
ch <- 1 // 阻塞,直到有接收者
}()
value := <-ch // 阻塞,直到有發送者
2. 有緩衝 Channel(非同步)
ch := make(chan int, 3) // 緩衝大小為 3
ch <- 1 // 不阻塞
ch <- 2 // 不阻塞
ch <- 3 // 不阻塞
ch <- 4 // 阻塞!緩衝已滿
單向 Channel
// 只能發送
func send(ch chan<- int) {
ch <- 42
}
// 只能接收
func receive(ch <-chan int) {
value := <-ch
fmt.Println(value)
}
Select 多路複用
ch1 := make(chan int)
ch2 := make(chan string)
select {
case v1 := <-ch1:
fmt.Println("收到整數:", v1)
case v2 := <-ch2:
fmt.Println("收到字串:", v2)
case <-time.After(1 * time.Second):
fmt.Println("超時")
}
實際範例
範例 1:並發下載
package main
import (
"fmt"
"sync"
"time"
)
func download(url string, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("開始下載: %s\n", url)
time.Sleep(2 * time.Second) // 模擬下載
fmt.Printf("完成下載: %s\n", url)
}
func main() {
urls := []string{
"http://example.com/file1",
"http://example.com/file2",
"http://example.com/file3",
}
var wg sync.WaitGroup
for _, url := range urls {
wg.Add(1)
go download(url, &wg)
}
wg.Wait() // 等待所有下載完成
fmt.Println("所有檔案下載完成")
}
範例 2:生產者-消費者模式
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 1; i <= 5; i++ {
fmt.Printf("生產: %d\n", i)
ch <- i
time.Sleep(time.Second)
}
close(ch) // 關閉 channel
}
func consumer(ch <-chan int) {
for num := range ch { // 持續接收直到 channel 關閉
fmt.Printf("消費: %d\n", num)
}
}
func main() {
ch := make(chan int, 2) // 緩衝大小為 2
go producer(ch)
consumer(ch)
fmt.Println("完成")
}
範例 3:Worker Pool
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for job := range jobs {
fmt.Printf("Worker %d 處理任務 %d\n", id, job)
results <- job * 2 // 處理並回傳結果
}
}
func main() {
const numJobs = 10
const numWorkers = 3
jobs := make(chan int, numJobs)
results := make(chan int, numJobs)
var wg sync.WaitGroup
// 啟動 workers
for w := 1; w <= numWorkers; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
// 發送任務
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs)
// 等待所有 workers 完成
wg.Wait()
close(results)
// 收集結果
for result := range results {
fmt.Println("結果:", result)
}
}
最佳實踐
1. 永遠等待 Goroutines 完成
// ❌ 錯誤:main 可能提前結束
func main() {
go doSomething()
}
// ✅ 正確:使用 WaitGroup
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doSomething()
}()
wg.Wait()
}
2. 避免 Goroutine 洩漏
// ❌ 錯誤:goroutine 永遠阻塞
func leak() {
ch := make(chan int)
go func() {
val := <-ch // 永遠等待
fmt.Println(val)
}()
// 忘記發送資料到 ch
}
// ✅ 正確:使用 context 或 timeout
func noLeak(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-ctx.Done():
return // 可以退出
}
}()
}
3. 適當使用緩衝 Channel
// 非同步發送,不阻塞發送者
ch := make(chan int, 10)
// 同步發送,確保資料被接收
ch := make(chan int)
4. 關閉 Channel 的時機
// 原則:由發送者關閉 channel,接收者不要關閉
// ✅ 正確
func producer(ch chan<- int) {
defer close(ch)
for i := 0; i < 5; i++ {
ch <- i
}
}
// ❌ 錯誤:接收者不應該關閉
func consumer(ch <-chan int) {
for val := range ch {
fmt.Println(val)
}
close(ch) // 錯誤!
}
5. 使用 Context 控制 Goroutine 生命週期
func worker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("收到取消信號")
return
default:
// 執行工作
time.Sleep(time.Second)
}
}
}
func main() {
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
go worker(ctx)
time.Sleep(10 * time.Second) // 5 秒後 worker 會自動停止
}
常見陷阱
1. 迴圈變數陷阱
// ❌ 錯誤:所有 goroutines 都會使用最後一個 i 的值
for i := 0; i < 5; i++ {
go func() {
fmt.Println(i) // 可能全部印出 5
}()
}
// ✅ 正確:傳遞參數
for i := 0; i < 5; i++ {
go func(n int) {
fmt.Println(n) // 正確印出 0, 1, 2, 3, 4
}(i)
}
// ✅ 正確:使用區域變數(Go 1.22+)
for i := 0; i < 5; i++ {
i := i // 建立新的區域變數
go func() {
fmt.Println(i)
}()
}
2. Race Condition(競態條件)
// ❌ 錯誤:多個 goroutines 同時修改共享變數
var counter int
for i := 0; i < 1000; i++ {
go func() {
counter++ // 不安全
}()
}
// ✅ 正確:使用 Mutex
var counter int
var mu sync.Mutex
for i := 0; i < 1000; i++ {
go func() {
mu.Lock()
counter++
mu.Unlock()
}()
}
// ✅ 更好:使用 atomic
var counter int64
for i := 0; i < 1000; i++ {
go func() {
atomic.AddInt64(&counter, 1)
}()
}
3. Channel 死鎖
// ❌ 錯誤:發送到無緩衝 channel 但沒有接收者
ch := make(chan int)
ch <- 1 // fatal error: all goroutines are asleep - deadlock!
// ✅ 正確:在 goroutine 中發送
ch := make(chan int)
go func() {
ch <- 1
}()
value := <-ch
4. 忘記 defer wg.Done()
// ❌ 錯誤:如果函數 panic,wg.Done() 不會被呼叫
func worker(wg *sync.WaitGroup) {
wg.Done() // 如果前面 panic,這行不會執行
doSomething()
}
// ✅ 正確:使用 defer 確保一定會執行
func worker(wg *sync.WaitGroup) {
defer wg.Done()
doSomething()
}
5. 向已關閉的 Channel 發送資料
// ❌ 錯誤:panic: send on closed channel
ch := make(chan int)
close(ch)
ch <- 1 // panic!
// ✅ 正確:確保不會向已關閉的 channel 發送
// 通常由發送者負責關閉,接收者只讀取
效能考量
何時使用 Goroutines?
適合使用:
- I/O 密集型任務(網路請求、檔案讀寫)
- 可並行化的 CPU 密集型任務
- 需要非同步處理的場景
不適合使用:
- 簡單的函數呼叫(overhead 大於收益)
- 已經在迴圈中的快速操作
效能最佳化建議
- 限制 Goroutine 數量:使用 worker pool 模式
- 使用 buffered channel:減少阻塞
- 避免過度同步:只在必要時使用 mutex
- 使用 atomic 操作:取代簡單的 mutex
- 檢測 race condition:使用
go run -race
除錯工具
1. Race Detector(競態檢測器)
go run -race main.go
go test -race
go build -race
2. 查看 Goroutine 數量
fmt.Println("Goroutines:", runtime.NumGoroutine())
3. 設定 GOMAXPROCS
// 設定使用的 CPU 核心數
runtime.GOMAXPROCS(4)
// 或使用環境變數
// GOMAXPROCS=4 go run main.go
總結
核心概念
- Goroutine = 輕量級執行單元(2KB 起始)
- GMP 模型 = Go 調度器的核心(Goroutine, Machine, Processor)
- Channel = Goroutines 之間的通訊機制(CSP 模型)
- 不要通過共享記憶體來通訊,而是通過通訊來共享記憶體
記住這些原則
- 永遠等待 goroutines 完成(使用
WaitGroup或channel) - 由發送者關閉 channel
- 使用
-race檢測競態條件 - 避免 goroutine 洩漏(使用
context) - 傳遞參數給 goroutine 避免閉包陷阱
最佳實踐
1. 永遠等待 Goroutines 完成
✅ 正確做法:使用 WaitGroup
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
doSomething()
}()
wg.Wait()
❌ 錯誤做法:main 可能提前結束
func main() {
go doSomething()
}
2. 避免 Goroutine 洩漏
✅ 正確做法:使用 context 或 timeout
func noLeak(ctx context.Context) {
ch := make(chan int)
go func() {
select {
case val := <-ch:
fmt.Println(val)
case <-ctx.Done():
return
}
}()
}
❌ 錯誤做法:goroutine 永遠阻塞
func leak() {
ch := make(chan int)
go func() {
val := <-ch // 永遠等待
fmt.Println(val)
}()
}
3. 適當使用緩衝 Channel
✅ 非同步發送
ch := make(chan int, 10)
✅ 同步發送
ch := make(chan int)
4. 關閉 Channel 的時機
✅ 原則:由發送者關閉 channel
5. 使用 Context 控制生命週期
建立日期:2025-11-03 最後更新:2025-11-18