目錄
- 什麼是 sync 套件?
- sync.Once - 確保只執行一次
- sync.WaitGroup - 等待 Goroutine 完成
- sync.Mutex 和 sync.RWMutex - 互斥鎖
- sync.Cond - 條件變數
- sync.Map - 併發安全的 Map
- sync.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 適用於:
- 鍵只寫入一次,但讀取多次(寫少讀多)
- 多個 Goroutine 讀取、寫入和覆蓋不同的鍵
❌ sync.Map 不適用於:
- 頻繁增刪改的場景
- 需要範圍查詢
- 對效能要求極高的場景
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