Go sync 同步原語完全指南

深入理解 Go 語言 sync 套件中的同步原語,包含 Once、WaitGroup、Mutex、Cond、Map、Pool 及 atomic 操作


目錄


什麼是 sync 套件?

sync 套件 是 Go 語言提供的低階同步原語集合,用於在多個 Goroutine 之間安全地同步和共享資料。

為什麼需要 sync 套件?

Go 的哲學是「透過通訊來共享記憶體(使用 Channel)」,但在某些情況下,使用 sync 套件的同步原語會更高效:

Channel 適用場景:
  ├─ Goroutine 間的資料傳遞
  ├─ 任務分發和結果收集
  └─ 事件通知

sync 套件適用場景:
  ├─ 簡單的計數器
  ├─ 快取和共享狀態
  ├─ 初始化一次性資源
  └─ 效能關鍵路徑

sync 套件核心組件

sync.Once        // 確保函數只執行一次
sync.WaitGroup   // 等待一組 Goroutine 完成
sync.Mutex       // 互斥鎖
sync.RWMutex     // 讀寫鎖
sync.Cond        // 條件變數
sync.Map         // 併發安全的 Map
sync.Pool        // 物件池(減少 GC 壓力)

sync.Once - 確保只執行一次

什麼是 sync.Once?

sync.Once 是一個同步原語,用於確保某個函數只執行一次,無論有多少個 Goroutine 同時呼叫。

核心特性

特性:
  ├─ 執行一次:無論呼叫多少次,函數只執行一次
  ├─ 執行緒安全:多個 Goroutine 可以安全地同時呼叫
  ├─ 阻塞等待:第一個呼叫執行時,其他呼叫會阻塞等待
  └─ 零值可用:不需要初始化即可使用

基本用法

package main

import (
    "fmt"
    "sync"
)

var once sync.Once

func initialize() {
    fmt.Println("初始化操作只執行一次")
}

func main() {
    // 多個 Goroutine 同時呼叫
    for i := 0; i < 10; i++ {
        go func() {
            once.Do(initialize)  // 只有第一個呼叫會執行
        }()
    }

    // 等待一下
    time.Sleep(time.Second)
}

// 輸出:初始化操作只執行一次(只印一次)

使用場景

1. 單例模式(Singleton Pattern)

type Database struct {
    conn *sql.DB
}

var (
    instance *Database
    once     sync.Once
)

// GetInstance 返回單例實例
func GetInstance() *Database {
    once.Do(func() {
        // 只執行一次的初始化邏輯
        instance = &Database{
            conn: createConnection(),
        }
        fmt.Println("資料庫連線建立")
    })
    return instance
}

// 多個 Goroutine 呼叫都會得到同一個實例
func main() {
    for i := 0; i < 10; i++ {
        go func() {
            db := GetInstance()  // 所有呼叫得到相同實例
            _ = db
        }()
    }
}

2. 一次性初始化配置

var (
    config     *Config
    configOnce sync.Once
)

func loadConfig() *Config {
    configOnce.Do(func() {
        fmt.Println("載入配置檔案")
        config = &Config{
            // 從檔案或環境變數讀取
        }
    })
    return config
}

3. 延遲初始化(Lazy Initialization)

type ExpensiveResource struct {
    data []byte
}

var (
    resource     *ExpensiveResource
    resourceOnce sync.Once
)

func getResource() *ExpensiveResource {
    resourceOnce.Do(func() {
        fmt.Println("建立昂貴的資源(只建立一次)")
        resource = &ExpensiveResource{
            data: make([]byte, 1024*1024*100), // 100MB
        }
    })
    return resource
}

sync.Once 實作原理

// sync.Once 的內部結構(簡化版)
type Once struct {
    done uint32        // 標記是否已執行(使用 atomic 操作)
    m    Mutex         // 互斥鎖,保護初始化過程
}

func (o *Once) Do(f func()) {
    // 快速路徑:已執行過,直接返回
    if atomic.LoadUint32(&o.done) == 0 {
        // 慢速路徑:需要初始化
        o.doSlow(f)
    }
}

func (o *Once) doSlow(f func()) {
    o.m.Lock()
    defer o.m.Unlock()

    // 雙重檢查:可能其他 Goroutine 已經完成了初始化
    if o.done == 0 {
        defer atomic.StoreUint32(&o.done, 1)
        f()  // 執行初始化函數
    }
}

關鍵點

  • 使用 atomic 操作快速檢查是否已執行
  • 使用 Mutex 保護初始化過程
  • 雙重檢查鎖定模式(Double-Checked Locking)

注意事項

❌ 錯誤:重複使用 Once 執行不同函數

var once sync.Once

once.Do(func() {
    fmt.Println("第一個函數")
})

once.Do(func() {
    fmt.Println("第二個函數")  // 不會執行!
})

// 輸出:第一個函數

❌ 錯誤:在 Do 內部 panic 後重新呼叫

var once sync.Once

func dangerous() {
    once.Do(func() {
        panic("發生錯誤")  // panic 後,once 仍被標記為已執行
    })
}

// 後續呼叫不會重試

✅ 正確:每個初始化任務使用獨立的 Once

var (
    dbOnce     sync.Once
    cacheOnce  sync.Once
    db         *Database
    cache      *Cache
)

func initDB() {
    dbOnce.Do(func() {
        db = createDB()
    })
}

func initCache() {
    cacheOnce.Do(func() {
        cache = createCache()
    })
}

sync.WaitGroup - 等待 Goroutine 完成

什麼是 sync.WaitGroup?

sync.WaitGroup 用於等待一組 Goroutine 完成執行,類似於計數信號量。

核心方法

type WaitGroup struct {
    // 內部計數器
}

func (wg *WaitGroup) Add(delta int)  // 增加計數器
func (wg *WaitGroup) Done()          // 減少計數器(等同於 Add(-1))
func (wg *WaitGroup) Wait()          // 阻塞等待計數器歸零

基本用法

package main

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

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()  // 完成時減少計數器

    fmt.Printf("Worker %d 開始工作\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d 完成工作\n", id)
}

func main() {
    var wg sync.WaitGroup

    // 啟動 5 個 worker
    for i := 1; i <= 5; i++ {
        wg.Add(1)  // 增加計數器
        go worker(i, &wg)
    }

    // 等待所有 worker 完成
    wg.Wait()
    fmt.Println("所有 worker 已完成")
}

輸出

Worker 1 開始工作
Worker 2 開始工作
Worker 3 開始工作
Worker 4 開始工作
Worker 5 開始工作
Worker 1 完成工作
Worker 2 完成工作
...
所有 worker 已完成

使用場景

1. 並發任務等待

func processFiles(files []string) {
    var wg sync.WaitGroup

    for _, file := range files {
        wg.Add(1)
        go func(f string) {
            defer wg.Done()
            processFile(f)
        }(file)
    }

    wg.Wait()  // 等待所有檔案處理完成
    fmt.Println("所有檔案處理完成")
}

2. 資料批次處理

func batchProcess(items []Item) []Result {
    var wg sync.WaitGroup
    results := make([]Result, len(items))

    for i, item := range items {
        wg.Add(1)
        go func(idx int, it Item) {
            defer wg.Done()
            results[idx] = process(it)
        }(i, item)
    }

    wg.Wait()
    return results
}

3. 限制並發數量

func processWithLimit(items []Item, maxConcurrency int) {
    var wg sync.WaitGroup
    semaphore := make(chan struct{}, maxConcurrency)

    for _, item := range items {
        wg.Add(1)
        go func(it Item) {
            defer wg.Done()

            semaphore <- struct{}{}        // 獲取信號量
            defer func() { <-semaphore }() // 釋放信號量

            process(it)
        }(item)
    }

    wg.Wait()
}

注意事項

❌ 錯誤:在 Goroutine 內部 Add

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    go func() {
        wg.Add(1)  // ❌ 錯誤:可能在 Wait() 之後才執行
        defer wg.Done()
        // 工作
    }()
}

wg.Wait()  // 可能提前返回

✅ 正確:在啟動 Goroutine 前 Add

var wg sync.WaitGroup

for i := 0; i < 5; i++ {
    wg.Add(1)  // ✅ 正確:在啟動前增加計數
    go func() {
        defer wg.Done()
        // 工作
    }()
}

wg.Wait()

❌ 錯誤:重複使用 WaitGroup

var wg sync.WaitGroup

// 第一次使用
wg.Add(1)
go func() { defer wg.Done(); work1() }()
wg.Wait()

// ❌ 錯誤:直接重複使用(可能有競爭條件)
wg.Add(1)
go func() { defer wg.Done(); work2() }()
wg.Wait()

✅ 正確:每次創建新的 WaitGroup 或確保計數器歸零

// 方法 1:每次創建新的
func process1() {
    wg := sync.WaitGroup{}
    wg.Add(1)
    go func() { defer wg.Done(); work() }()
    wg.Wait()
}

// 方法 2:確保上一次已完成
var wg sync.WaitGroup
wg.Wait()  // 確保計數器為 0
wg.Add(1)
go func() { defer wg.Done(); work() }()

sync.Mutex 和 sync.RWMutex - 互斥鎖

簡介

詳細的鎖機制說明請參考 Lock 鎖機制完整指南

這裡僅提供快速參考:

sync.Mutex - 互斥鎖

var mu sync.Mutex
var counter int

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

特點

  • 不可重入(同一 Goroutine 重複 Lock 會死鎖)
  • 零值可用
  • 不能複製已使用的 Mutex

sync.RWMutex - 讀寫鎖

var rwMu sync.RWMutex
var data string

// 讀操作
func read() string {
    rwMu.RLock()
    defer rwMu.RUnlock()
    return data
}

// 寫操作
func write(value string) {
    rwMu.Lock()
    defer rwMu.Unlock()
    data = value
}

特點

  • 允許多個讀者同時存取
  • 寫者獨佔存取
  • 適合讀多寫少的場景

鎖的相容性

         讀鎖    寫鎖
讀鎖      ✅      ❌
寫鎖      ❌      ❌

sync.Cond - 條件變數

什麼是 sync.Cond?

sync.Cond 是條件變數,用於 Goroutine 間的等待/通知機制。

核心方法

type Cond struct {
    L Locker  // 關聯的鎖(通常是 *Mutex)
    // ...
}

func NewCond(l Locker) *Cond       // 創建條件變數
func (c *Cond) Wait()              // 等待條件
func (c *Cond) Signal()            // 喚醒一個等待的 Goroutine
func (c *Cond) Broadcast()         // 喚醒所有等待的 Goroutine

基本用法

package main

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

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    ready := false

    // 消費者:等待條件滿足
    go func() {
        mu.Lock()
        for !ready {  // 必須使用 for 循環檢查條件
            cond.Wait()  // 等待通知
        }
        fmt.Println("條件已滿足,開始執行")
        mu.Unlock()
    }()

    // 生產者:設定條件並通知
    time.Sleep(time.Second)
    mu.Lock()
    ready = true
    cond.Signal()  // 通知等待的 Goroutine
    mu.Unlock()

    time.Sleep(time.Second)
}

使用場景:生產者-消費者模型

type Queue struct {
    mu    sync.Mutex
    cond  *sync.Cond
    items []interface{}
    cap   int
}

func NewQueue(capacity int) *Queue {
    q := &Queue{
        items: make([]interface{}, 0),
        cap:   capacity,
    }
    q.cond = sync.NewCond(&q.mu)
    return q
}

// 生產者:加入項目
func (q *Queue) Enqueue(item interface{}) {
    q.mu.Lock()
    defer q.mu.Unlock()

    // 等待佇列不滿
    for len(q.items) >= q.cap {
        q.cond.Wait()
    }

    q.items = append(q.items, item)
    q.cond.Signal()  // 通知消費者
}

// 消費者:取出項目
func (q *Queue) Dequeue() interface{} {
    q.mu.Lock()
    defer q.mu.Unlock()

    // 等待佇列不空
    for len(q.items) == 0 {
        q.cond.Wait()
    }

    item := q.items[0]
    q.items = q.items[1:]
    q.cond.Signal()  // 通知生產者
    return item
}

注意事項

⚠️ 必須使用 for 循環檢查條件

// ❌ 錯誤:使用 if
mu.Lock()
if !ready {
    cond.Wait()  // 可能虛假喚醒
}
mu.Unlock()

// ✅ 正確:使用 for
mu.Lock()
for !ready {  // 被喚醒後重新檢查條件
    cond.Wait()
}
mu.Unlock()

⚠️ Wait 之前必須持有鎖

// ❌ 錯誤:沒有加鎖
cond.Wait()  // panic: sync: unlock of unlocked mutex

// ✅ 正確:先加鎖
mu.Lock()
cond.Wait()
mu.Unlock()

sync.Map - 併發安全的 Map

什麼是 sync.Map?

sync.Map 是 Go 提供的併發安全的 Map 實作,適用於特定場景。

核心方法

type Map struct {
    // ...
}

func (m *Map) Store(key, value interface{})     // 儲存鍵值對
func (m *Map) Load(key interface{}) (value interface{}, ok bool)  // 讀取值
func (m *Map) LoadOrStore(key, value interface{}) (actual interface{}, loaded bool)
func (m *Map) Delete(key interface{})           // 刪除鍵
func (m *Map) Range(f func(key, value interface{}) bool)  // 遍歷

基本用法

package main

import (
    "fmt"
    "sync"
)

func main() {
    var m sync.Map

    // 儲存
    m.Store("name", "Alice")
    m.Store("age", 30)

    // 讀取
    if value, ok := m.Load("name"); ok {
        fmt.Println("Name:", value)
    }

    // LoadOrStore:如果不存在則儲存
    actual, loaded := m.LoadOrStore("city", "Taipei")
    fmt.Println("City:", actual, "Already exists:", loaded)

    // 刪除
    m.Delete("age")

    // 遍歷
    m.Range(func(key, value interface{}) bool {
        fmt.Printf("%v: %v\n", key, value)
        return true  // 繼續遍歷
    })
}

適用場景

✅ sync.Map 適用於

  1. 鍵只寫入一次,但讀取多次(寫少讀多)
  2. 多個 Goroutine 讀取、寫入和覆蓋不同的鍵

❌ sync.Map 不適用於

  1. 頻繁增刪改的場景
  2. 需要範圍查詢
  3. 對效能要求極高的場景

sync.Map vs map + Mutex

// 方案 1:sync.Map
var sm sync.Map
sm.Store("key", "value")
value, _ := sm.Load("key")

// 方案 2:map + RWMutex(通常更快)
var (
    m  = make(map[string]string)
    mu sync.RWMutex
)

mu.Lock()
m["key"] = "value"
mu.Unlock()

mu.RLock()
value := m["key"]
mu.RUnlock()

選擇建議

  • 大多數情況:使用 map + RWMutex(更快、更直觀)
  • 特定場景:符合上述適用場景時使用 sync.Map

實戰範例:快取

type Cache struct {
    data sync.Map
}

func (c *Cache) Get(key string) (interface{}, bool) {
    return c.data.Load(key)
}

func (c *Cache) Set(key string, value interface{}) {
    c.data.Store(key, value)
}

func (c *Cache) GetOrSet(key string, factory func() interface{}) interface{} {
    if value, ok := c.data.Load(key); ok {
        return value
    }

    // 使用 LoadOrStore 避免重複計算
    value := factory()
    actual, _ := c.data.LoadOrStore(key, value)
    return actual
}

sync.Pool - 物件池

什麼是 sync.Pool?

sync.Pool 是臨時物件池,用於儲存和重複使用臨時物件,減少記憶體分配和 GC 壓力。

核心特性

特性:
  ├─ 自動清理:GC 時會清空 Pool
  ├─ 執行緒安全:多個 Goroutine 可安全使用
  ├─ 無容量限制:可以無限增長
  └─ 不保證儲存:可能隨時被清理

基本用法

package main

import (
    "bytes"
    "fmt"
    "sync"
)

var bufferPool = sync.Pool{
    New: func() interface{} {
        // 建立新物件的工廠函數
        return new(bytes.Buffer)
    },
}

func processData(data string) string {
    // 從 Pool 獲取 buffer
    buf := bufferPool.Get().(*bytes.Buffer)
    defer bufferPool.Put(buf)  // 使用完後放回 Pool

    buf.Reset()  // 重置 buffer
    buf.WriteString("Processed: ")
    buf.WriteString(data)

    return buf.String()
}

func main() {
    fmt.Println(processData("Hello"))
    fmt.Println(processData("World"))
}

使用場景

1. 重複使用緩衝區

var bufferPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func handleRequest(w http.ResponseWriter, r *http.Request) {
    buf := bufferPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset()
        bufferPool.Put(buf)
    }()

    // 使用 buffer 處理請求
    buf.WriteString("Response data")
    w.Write(buf.Bytes())
}

2. 大型結構體重複使用

type BigStruct struct {
    data [1024 * 1024]byte  // 1MB
}

var bigStructPool = sync.Pool{
    New: func() interface{} {
        return new(BigStruct)
    },
}

func processBigData() {
    obj := bigStructPool.Get().(*BigStruct)
    defer bigStructPool.Put(obj)

    // 使用 obj 處理資料
}

注意事項

⚠️ Pool 中的物件可能隨時被清理

pool := sync.Pool{
    New: func() interface{} {
        return make([]byte, 1024)
    },
}

buf := pool.Get().([]byte)
pool.Put(buf)

// GC 可能會清空 Pool
runtime.GC()

// 再次 Get 可能獲得新物件(New 會被呼叫)
buf2 := pool.Get().([]byte)

⚠️ 放回 Pool 前必須重置狀態

// ❌ 錯誤:沒有重置
buf := bufferPool.Get().(*bytes.Buffer)
buf.WriteString("data")
bufferPool.Put(buf)  // 下次獲取會包含舊資料

// ✅ 正確:重置後放回
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
    buf.Reset()  // 重置
    bufferPool.Put(buf)
}()

atomic - 原子操作

什麼是 atomic?

sync/atomic 套件提供低階的原子記憶體操作,用於實作無鎖的資料結構。

核心操作

// 載入(Load)
atomic.LoadInt32(&value)
atomic.LoadInt64(&value)
atomic.LoadUint32(&value)
atomic.LoadPointer(&ptr)

// 儲存(Store)
atomic.StoreInt32(&value, newValue)
atomic.StoreInt64(&value, newValue)

// 增加(Add)
atomic.AddInt32(&value, delta)
atomic.AddInt64(&value, delta)

// 比較並交換(Compare-And-Swap)
atomic.CompareAndSwapInt32(&value, old, new)
atomic.CompareAndSwapInt64(&value, old, new)

// 交換(Swap)
atomic.SwapInt32(&value, new)

基本用法

package main

import (
    "fmt"
    "sync"
    "sync/atomic"
)

func main() {
    var counter int64
    var wg sync.WaitGroup

    // 啟動 1000 個 Goroutine 同時增加計數器
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            atomic.AddInt64(&counter, 1)
        }()
    }

    wg.Wait()
    fmt.Println("Counter:", atomic.LoadInt64(&counter))  // 輸出:Counter: 1000
}

使用場景

1. 無鎖計數器

type Counter struct {
    value int64
}

func (c *Counter) Increment() {
    atomic.AddInt64(&c.value, 1)
}

func (c *Counter) Get() int64 {
    return atomic.LoadInt64(&c.value)
}

2. 狀態標記

type Server struct {
    running int32  // 0 = stopped, 1 = running
}

func (s *Server) Start() bool {
    // 使用 CAS 確保只啟動一次
    return atomic.CompareAndSwapInt32(&s.running, 0, 1)
}

func (s *Server) Stop() {
    atomic.StoreInt32(&s.running, 0)
}

func (s *Server) IsRunning() bool {
    return atomic.LoadInt32(&s.running) == 1
}

3. 實作自旋鎖(Spinlock)

type SpinLock struct {
    locked uint32
}

func (l *SpinLock) Lock() {
    for !atomic.CompareAndSwapUint32(&l.locked, 0, 1) {
        // 自旋等待
        runtime.Gosched()  // 讓出 CPU
    }
}

func (l *SpinLock) Unlock() {
    atomic.StoreUint32(&l.locked, 0)
}

atomic 的限制

❌ 不支援的操作

  • 無法對 bool 直接操作(需轉換為 int32
  • 無法對浮點數操作(需使用 math.Float64bits
  • 無法對複雜結構體操作

✅ 解決方案

// bool → int32
var flag int32  // 0 = false, 1 = true
atomic.StoreInt32(&flag, 1)

// float64 → uint64
import "math"
var f float64 = 3.14
bits := math.Float64bits(f)
atomic.StoreUint64(&bits, math.Float64bits(2.71))

實戰範例

範例 1:併發安全的計數器(多種實作)

// 方案 1:使用 Mutex
type MutexCounter struct {
    mu    sync.Mutex
    value int
}

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

// 方案 2:使用 atomic
type AtomicCounter struct {
    value int64
}

func (c *AtomicCounter) Increment() {
    atomic.AddInt64(&c.value, 1)
}

// 效能比較:atomic 更快,但只適合簡單操作

範例 2:單例模式(使用 sync.Once)

type Config struct {
    DatabaseURL string
    Port        int
}

var (
    config *Config
    once   sync.Once
)

func GetConfig() *Config {
    once.Do(func() {
        fmt.Println("載入配置(只執行一次)")
        config = &Config{
            DatabaseURL: "localhost:5432",
            Port:        8080,
        }
    })
    return config
}

範例 3:併發 HTTP 請求處理

func fetchURLs(urls []string) []Result {
    var wg sync.WaitGroup
    results := make([]Result, len(urls))

    for i, url := range urls {
        wg.Add(1)
        go func(index int, u string) {
            defer wg.Done()

            resp, err := http.Get(u)
            if err != nil {
                results[index] = Result{URL: u, Error: err}
                return
            }
            defer resp.Body.Close()

            body, _ := io.ReadAll(resp.Body)
            results[index] = Result{
                URL:  u,
                Data: body,
            }
        }(i, url)
    }

    wg.Wait()
    return results
}

範例 4:使用 sync.Pool 優化記憶體

var jsonEncoderPool = sync.Pool{
    New: func() interface{} {
        return json.NewEncoder(new(bytes.Buffer))
    },
}

func encodeJSON(data interface{}) ([]byte, error) {
    encoder := jsonEncoderPool.Get().(*json.Encoder)
    buf := encoder.Writer().(*bytes.Buffer)

    defer func() {
        buf.Reset()
        jsonEncoderPool.Put(encoder)
    }()

    if err := encoder.Encode(data); err != nil {
        return nil, err
    }

    return buf.Bytes(), nil
}

範例 5:限流器(使用 atomic)

type RateLimiter struct {
    tokens    int64
    rate      int64  // 每秒恢復的 token 數
    lastTime  int64  // 上次補充時間(Unix timestamp)
}

func NewRateLimiter(rate int64) *RateLimiter {
    return &RateLimiter{
        tokens:   rate,
        rate:     rate,
        lastTime: time.Now().Unix(),
    }
}

func (rl *RateLimiter) Allow() bool {
    now := time.Now().Unix()
    lastTime := atomic.LoadInt64(&rl.lastTime)

    // 計算應該補充的 token
    elapsed := now - lastTime
    tokensToAdd := elapsed * rl.rate

    if tokensToAdd > 0 {
        // 補充 token
        atomic.AddInt64(&rl.tokens, tokensToAdd)
        atomic.StoreInt64(&rl.lastTime, now)
    }

    // 嘗試消耗一個 token
    for {
        tokens := atomic.LoadInt64(&rl.tokens)
        if tokens <= 0 {
            return false  // 沒有可用 token
        }
        if atomic.CompareAndSwapInt64(&rl.tokens, tokens, tokens-1) {
            return true  // 成功消耗 token
        }
        // CAS 失敗,重試
    }
}

最佳實踐

1. 優先使用 Channel,必要時才用 sync

// ✅ 推薦:使用 Channel
func process(jobs <-chan Job, results chan<- Result) {
    for job := range jobs {
        results <- processJob(job)
    }
}

// ⚠️ 必要時:使用 sync(效能關鍵或簡單計數)
var counter int64
atomic.AddInt64(&counter, 1)

Go 哲學

"Don't communicate by sharing memory;
 share memory by communicating."

2. 總是使用 defer 釋放鎖

// ✅ 正確:使用 defer
func update() {
    mu.Lock()
    defer mu.Unlock()

    // 即使 panic 也會釋放鎖
    doSomething()
}

// ❌ 錯誤:手動釋放(可能忘記)
func update() {
    mu.Lock()
    doSomething()
    mu.Unlock()  // 如果 doSomething() panic,鎖永遠不釋放
}

3. 避免在持有鎖時做耗時操作

// ❌ 錯誤:持有鎖時做 I/O
mu.Lock()
data := readFromDatabase()  // 耗時操作
cache[key] = data
mu.Unlock()

// ✅ 正確:只在必要時持有鎖
data := readFromDatabase()  // 不需要鎖

mu.Lock()
cache[key] = data  // 只鎖這部分
mu.Unlock()

4. 使用 go vet 檢查常見錯誤

# 檢查是否複製了 Mutex
go vet ./...

# 常見錯誤:
# - 複製含有 Mutex 的結構體
# - WaitGroup 誤用
# - atomic 對齊問題

5. 選擇合適的同步原語

簡單計數器 → atomic
共享狀態保護 → Mutex
讀多寫少 → RWMutex
只執行一次 → sync.Once
等待完成 → WaitGroup
條件等待 → Cond
臨時物件 → Pool
特定場景的 Map → sync.Map
Goroutine 通訊 → Channel

6. 注意記憶體對齊(atomic 操作)

// ❌ 可能有對齊問題
type Counter struct {
    flag  bool
    count int64  // 可能未 8 字節對齊
}

// ✅ 確保對齊
type Counter struct {
    count int64  // 放在前面,確保 8 字節對齊
    flag  bool
}

// 或使用 _ 填充
type Counter struct {
    flag  bool
    _     [7]byte  // 填充
    count int64
}

常見問題

問題 1:sync.Once 可以重置嗎?

答案:不可以,一旦執行過就無法重置。

解決方案:如需多次執行,使用新的 sync.Once 實例。

// ❌ 無法重置
var once sync.Once
once.Do(func() { fmt.Println("First") })
// 無法重置 once

// ✅ 使用新實例
func doSomething() {
    once := sync.Once{}  // 新實例
    once.Do(func() { fmt.Println("Can execute again") })
}

問題 2:為什麼 WaitGroup 計數器會是負數?

常見原因:Done() 呼叫次數多於 Add()

var wg sync.WaitGroup

wg.Add(1)
wg.Done()
wg.Done()  // ❌ 錯誤:多呼叫一次 Done()
// panic: sync: negative WaitGroup counter

解決方案

  • 確保每個 Add() 對應一個 Done()
  • 使用 defer 確保 Done() 被呼叫
  • 仔細檢查錯誤處理路徑

問題 3:sync.Map 和 map + Mutex 哪個更快?

答案:大多數情況下 map + RWMutex 更快

效能對比

寫少讀多:sync.Map 可能更快
寫多讀少:map + Mutex 更快
頻繁增刪:map + Mutex 更快
一般場景:map + RWMutex 更快

建議

  • 預設使用 map + RWMutex
  • 只在特定場景(鍵固定、讀多寫少)時使用 sync.Map
  • 實際測試驗證效能

問題 4:可以複製 Mutex 嗎?

答案:不可以!複製已使用的 Mutex 會導致未定義行為

// ❌ 錯誤:複製 Mutex
type Counter struct {
    mu    sync.Mutex
    value int
}

c1 := Counter{}
c1.mu.Lock()
c2 := c1  // ❌ 複製了 Mutex
c1.mu.Unlock()
c2.mu.Lock()  // 可能死鎖或 panic

// ✅ 正確:使用指標
type Counter struct {
    mu    sync.Mutex
    value int
}

c1 := &Counter{}
c2 := c1  // 指標賦值,不會複製 Mutex

檢測工具

go vet ./...  # 會檢測 Mutex 複製問題

問題 5:atomic 操作和 Mutex 哪個更快?

答案:atomic 更快,但功能有限

對比

操作 atomic Mutex 說明
簡單讀寫 ⚡ 極快 🐢 較慢 atomic 無鎖,Mutex 需要上下文切換
複合操作 ❌ 不支援 ✅ 支援 if value > 10 { value = 0 }
複雜邏輯 ❌ 不支援 ✅ 支援 需要保護多個變數

選擇建議

簡單計數器、標誌位 → atomic
需要保護多個變數 → Mutex
需要條件判斷 → Mutex

問題 6:Cond.Wait() 為什麼必須在迴圈中?

答案:防止虛假喚醒(Spurious Wakeup)

// ❌ 錯誤:使用 if
mu.Lock()
if !ready {
    cond.Wait()  // 可能虛假喚醒,條件未滿足
}
doWork()
mu.Unlock()

// ✅ 正確:使用 for
mu.Lock()
for !ready {  // 被喚醒後重新檢查條件
    cond.Wait()
}
doWork()
mu.Unlock()

虛假喚醒原因

  • 作業系統層級的喚醒
  • Signal 和 Broadcast 的競爭
  • 其他 Goroutine 改變了條件

總結

核心要點

sync 套件提供了低階同步原語:

once.Once       → 確保只執行一次(單例、初始化)
sync.WaitGroup  → 等待一組 Goroutine 完成
sync.Mutex      → 互斥鎖(基本保護)
sync.RWMutex    → 讀寫鎖(讀多寫少)
sync.Cond       → 條件變數(等待/通知)
sync.Map        → 併發安全的 Map(特定場景)
sync.Pool       → 物件池(減少 GC)
atomic          → 原子操作(無鎖,高效能)

選擇指南

需求 推薦方案
只執行一次 sync.Once
等待完成 WaitGroup
簡單計數 atomic
共享狀態 Mutex
讀多寫少 RWMutex
條件等待 Cond + Mutex
併發 Map map + RWMutex(優先)或 sync.Map
臨時物件 sync.Pool
Goroutine 通訊 Channel(優先)

最佳實踐總結

原則 說明
優先使用 Channel 符合 Go 哲學
總是用 defer 釋放鎖 防止死鎖
最小化鎖範圍 只鎖必要的程式碼
避免在鎖內做耗時操作 提高併發性能
使用 go vet 檢查 自動發現常見錯誤
選對同步原語 根據場景選擇
注意記憶體對齊 atomic 操作需要

常見錯誤

錯誤做法

  • 複製 Mutex 或 WaitGroup
  • 在 Goroutine 內部 WaitGroup.Add()
  • sync.Once 執行會 panic 的函數
  • 不使用 for 循環檢查 Cond 條件
  • 過度使用 sync.Map

正確做法

  • 傳遞指標,不複製同步原語
  • 在啟動 Goroutine 前 Add()
  • 確保 Once 執行的函數不會 panic
  • 總是用 for 循環檢查條件
  • 大多數情況用 map + RWMutex

建立日期:2025-11-24 最後更新:2025-11-24

🔗相關文章