本文 AI 產出,尚未審核

Golang 並發編程:Goroutine 的基本概念

簡介

在現代的網路服務、即時資料處理與分散式系統中,並發已成為提升效能與資源利用率的關鍵技術。Go 語言(Golang)自設計之初就把並發作為核心特性,透過輕量級的 goroutine 與通道(channel)讓開發者能以簡潔的語法寫出高效能的程式。
本單元將帶你從 什麼是 goroutine、如何 啟動與管理、以及在實務開發中常見的 陷阱與最佳實踐,一步步建立對 Go 並發模型的直觀認識,讓你能快速在專案中運用。


核心概念

1. Goroutine 是什麼?

  • Goroutine 是由 Go 執行階段(runtime)管理的輕量級執行單元。相較於作業系統的 thread,goroutine 只佔用數 KB 的堆疊空間,且會在需要時自動擴增或收縮。
  • 每個 Go 程式在 main 函式啟動時即有一個 主 goroutine,其他的 goroutine 皆由 go 關鍵字觸發。

重點:goroutine 的排程完全由 Go runtime 控制,開發者不需要直接與 OS thread 打交道。

2. 啟動 Goroutine

使用 go 關鍵字即可在新 goroutine 中執行函式或匿名函式:

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    fmt.Println("Hello from goroutine")
}

func main() {
    go sayHello() // <-- 啟動新 goroutine
    fmt.Println("Hello from main")
    time.Sleep(time.Second) // 防止程式提前結束
}

註解time.Sleep 只是一種最簡單的同步方式,實務上會使用 sync.WaitGroup、channel 或 context 來協調。

3. Goroutine 與同步(channel、WaitGroup)

3.1 使用 channel 傳遞資料

Channel 是 goroutine 之間安全傳遞資料的管道,具備 阻塞同步 的特性。

package main

import (
    "fmt"
)

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("worker %d 處理 job %d\n", id, j)
        results <- j * 2 // 假設工作是把數字乘以 2
    }
}

func main() {
    const numJobs = 5
    jobs := make(chan int, numJobs)
    results := make(chan int, numJobs)

    // 建立 3 個 worker goroutine
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // 送出工作
    for j := 1; j <= numJobs; j++ {
        jobs <- j
    }
    close(jobs) // 關閉 channel,讓 worker 知道沒有新工作

    // 收集結果
    for a := 0; a < numJobs; a++ {
        fmt.Println("結果:", <-results)
    }
}

3.2 使用 sync.WaitGroup 等待多個 goroutine 完成

WaitGroup 為最常見的同步工具之一,適合「啟動 N 個 goroutine,等全部結束」的情境。

package main

import (
    "fmt"
    "sync"
    "time"
)

func task(id int, wg *sync.WaitGroup) {
    defer wg.Done() // 任務結束時呼叫 Done
    fmt.Printf("Task %d 開始\n", id)
    time.Sleep(time.Duration(id) * time.Second)
    fmt.Printf("Task %d 完成\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 3; i++ {
        wg.Add(1)          // 計數器加 1
        go task(i, &wg)    // 啟動 goroutine
    }
    wg.Wait() // 等待所有 goroutine 完成
    fmt.Println("所有任務已完成")
}

3.3 使用 context 控制生命週期

context 用於在多層 goroutine 中傳遞取消訊號、截止時間或其他請求範圍的值。

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("worker %d 收到取消訊號,結束\n", id)
            return
        default:
            fmt.Printf("worker %d 正在執行\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    for i := 1; i <= 2; i++ {
        go worker(ctx, i)
    }

    // 等待 timeout 或手動 cancel
    <-ctx.Done()
    fmt.Println("主程式結束")
}

4. 調度與 GOMAXPROCS

Go runtime 會根據機器的 CPU 數量自動決定同時執行的 OS thread 數目。runtime.GOMAXPROCS 可手動調整:

package main

import (
    "fmt"
    "runtime"
)

func main() {
    fmt.Println("預設 GOMAXPROCS:", runtime.GOMAXPROCS(0))
    runtime.GOMAXPROCS(2) // 限制同時執行的 thread 數為 2
    fmt.Println("調整後 GOMAXPROCS:", runtime.GOMAXPROCS(0))
}

提示:在 CPU 密集型工作時,將 GOMAXPROCS 設為實體核心數可取得最佳效能;IO 密集型則可維持預設值。


常見陷阱與最佳實踐

陷阱 可能的問題 解決方式
忘記同步 主程式提前結束,導致 goroutine 尚未執行完畢 使用 sync.WaitGroup、channel 或 context 進行協調
資料競爭 (race condition) 多個 goroutine 同時寫同一變數,產生不可預期結果 使用 sync.Mutexsync.RWMutex 或 channel 進行互斥
過度產生 goroutine 產生過多 goroutine 會耗盡記憶體或導致排程開銷過大 控制併發數量(如使用 worker pool)
阻塞的 channel 未正確關閉 channel 或未接收,導致死鎖 確保所有發送端都有對應的接收端,必要時使用 select 超時
忘記 defer wg.Done() WaitGroup 計數不減,導致 wg.Wait() 永遠阻塞 在每個 goroutine 開頭使用 defer wg.Done(),或在所有路徑都呼叫 Done

最佳實踐

  1. 盡量使用 channel 作為通訊橋樑,讓資料流向自然、程式易於閱讀。
  2. 將共享資源封裝在結構體內,並提供方法保護(如使用 mutex 包裝的 type SafeCounter struct { mu sync.Mutex; v map[string]int })。
  3. 使用 context 來傳遞取消與截止時間,尤其在服務端處理 HTTP 請求時。
  4. 限制同時執行的 goroutine 數,可透過 buffered channel 或 semaphore 模式實作 worker pool。
  5. 在開發階段啟用 race detectorgo run -race ./...,即時捕捉競爭條件。

實際應用場景

場景 為何使用 Goroutine 範例
Web 伺服器 每個連線可獨立處理,提升併發量 net/http 內部已使用 goroutine 處理每個請求
資料流處理 (pipeline) 多階段處理可分為不同 goroutine,使用 channel 串接 圖像處理、日誌聚合
定時任務 / 背景工作 輕量級的 timer + goroutine 可實作 cron 功能 time.Ticker + goroutine
分散式爬蟲 同時抓取多個網頁,減少等待時間 worker pool + channel
即時訊息推送 多個客戶端同時接收訊息,使用 broadcast channel Pub/Sub 模式

範例:簡易的 Pub/Sub 系統

package main

import (
    "fmt"
    "sync"
    "time"
)

type PubSub struct {
    subs map[chan string]struct{}
    mu   sync.RWMutex
}

func NewPubSub() *PubSub {
    return &PubSub{
        subs: make(map[chan string]struct{}),
    }
}

func (ps *PubSub) Subscribe() <-chan string {
    ch := make(chan string, 1)
    ps.mu.Lock()
    ps.subs[ch] = struct{}{}
    ps.mu.Unlock()
    return ch
}

func (ps *PubSub) Unsubscribe(ch <-chan string) {
    ps.mu.Lock()
    delete(ps.subs, ch.(chan string))
    ps.mu.Unlock()
    close(ch.(chan string))
}

func (ps *PubSub) Publish(msg string) {
    ps.mu.RLock()
    for ch := range ps.subs {
        ch <- msg
    }
    ps.mu.RUnlock()
}

func main() {
    ps := NewPubSub()

    // 兩個訂閱者
    sub1 := ps.Subscribe()
    sub2 := ps.Subscribe()

    var wg sync.WaitGroup
    wg.Add(2)

    go func() {
        defer wg.Done()
        for m := range sub1 {
            fmt.Println("[Sub1] 收到:", m)
        }
    }()

    go func() {
        defer wg.Done()
        for m := range sub2 {
            fmt.Println("[Sub2] 收到:", m)
        }
    }()

    // 發布訊息
    for i := 1; i <= 3; i++ {
        ps.Publish(fmt.Sprintf("訊息 %d", i))
        time.Sleep(300 * time.Millisecond)
    }

    // 關閉訂閱
    ps.Unsubscribe(sub1)
    ps.Unsubscribe(sub2)

    wg.Wait()
    fmt.Println("Pub/Sub 結束")
}

此範例展示了 goroutine 搭配 channelsync.RWMutex 共同實作一個簡易的發布/訂閱機制,適合在即時聊天、事件驅動系統中使用。


總結

  • Goroutine 是 Go 語言的核心並發單元,輕量、易於建立,讓程式可以在同一時間執行多件事。
  • 透過 channel、sync.WaitGroup、context 等工具,我們可以安全、可控地協調多個 goroutine 的執行與生命週期。
  • 常見的陷阱包括忘記同步、資料競爭與過度產生 goroutine,遵循 「以 channel 為主、以 mutex 為輔」 的設計原則,可大幅降低錯誤風險。
  • 在 Web 伺服器、資料流水線、分散式爬蟲、即時訊息推送等場景,goroutine 都能提供高效且易於維護的解決方案。

掌握了 goroutine 的基本概念與實務技巧後,你就能在 Go 專案中自信地使用並發,寫出既 高效可讀 的程式碼。祝你在並發編程的旅程中玩得開心、寫得順利! 🚀