當 channel 不夠用時,sync 套件提供更傳統的鎖與同步原語。

Mutex(互斥鎖)#

import "sync"

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *Counter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

Mutex 使用注意事項

  1. 不要重複鎖定同一個 mutex(會死鎖)
  2. 不要解鎖未鎖定的 mutex(會 panic)
  3. 建議用 defer 確保解鎖

RWMutex(讀寫鎖)#

適用於讀多寫少的場景:

type Cache struct {
    mu   sync.RWMutex
    data map[string]string
}

// 讀操作使用讀鎖
func (c *Cache) Get(key string) string {
    c.mu.RLock()
    defer c.mu.RUnlock()
    return c.data[key]
}

// 寫操作使用寫鎖
func (c *Cache) Set(key, value string) {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.data[key] = value
}

RWMutex 允許多個讀取者同時持有讀鎖,但寫入者要獨占。讀遠多於寫時,效能顯著優於 Mutex。

WaitGroup#

用於等待一組 goroutine 完成:

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 5; i++ {
        wg.Add(1)  // 啟動 goroutine 前增加計數

        go func(id int) {
            defer wg.Done()  // 完成時減少計數
            fmt.Printf("Worker %d starting\n", id)
            time.Sleep(time.Second)
            fmt.Printf("Worker %d done\n", id)
        }(i)
    }

    wg.Wait()  // 阻塞直到計數歸零
    fmt.Println("All workers completed")
}

WaitGroup 使用規則

  • Add() 必須在 goroutine 啟動之前呼叫
  • Done() 應該在 goroutine 結束時呼叫(建議用 defer
  • Wait() 阻塞直到計數歸零
  • 計數不能變成負數(會 panic)

Once#

確保某段程式碼只執行一次:

var (
    instance *Config
    once     sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        // 只執行一次,即使多個 goroutine 同時呼叫
        instance = loadConfig()
    })
    return instance
}
Once 的實作原理

sync.Once 內部用 atomic 與 mutex 實作:

type Once struct {
    done uint32
    m    Mutex
}

func (o *Once) Do(f func()) {
    // 快速路徑:已執行過
    if atomic.LoadUint32(&o.done) == 1 {
        return
    }
    // 慢速路徑:競爭鎖
    o.m.Lock()
    defer o.m.Unlock()
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()
    }
}