Go Goroutines 完全指南

深入理解 Go 語言的核心特性:goroutine 的工作原理、調度機制、與執行緒的差異


目錄


什麼是 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 更輕量?

  1. 動態堆疊:起始只有 2KB,需要時自動增長
  2. 使用者空間調度:避免昂貴的系統呼叫
  3. 協作式調度:減少不必要的上下文切換
  4. 共享記憶體:所有 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 核心
+-------------------+

調度流程

  1. 建立 Goroutine:使用 go 關鍵字建立,進入 P 的本地佇列
  2. P 調度:P 從本地佇列取出 G 並分配給 M 執行
  3. 執行:M 執行 G 的程式碼
  4. 阻塞處理
    • 若 G 阻塞(如 I/O),M 會被釋放給其他 G 使用
    • 阻塞的 G 會被移到等待佇列
  5. 工作竊取:若 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 大於收益)
  • 已經在迴圈中的快速操作

效能最佳化建議

  1. 限制 Goroutine 數量:使用 worker pool 模式
  2. 使用 buffered channel:減少阻塞
  3. 避免過度同步:只在必要時使用 mutex
  4. 使用 atomic 操作:取代簡單的 mutex
  5. 檢測 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 模型)
  • 不要通過共享記憶體來通訊,而是通過通訊來共享記憶體

記住這些原則

  1. 永遠等待 goroutines 完成(使用 WaitGroupchannel
  2. 由發送者關閉 channel
  3. 使用 -race 檢測競態條件
  4. 避免 goroutine 洩漏(使用 context
  5. 傳遞參數給 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

🔗相關文章