Skip to content

sync 包:WaitGroup、Once、Mutex

前几篇我们聚焦于 channel,但 Go 并不只有 channel 一种同步工具。标准库 sync 包提供了一系列传统同步原语:WaitGroup、Once、Mutex、RWMutex、Cond、Map、Pool。它们与 channel 互补,在保护共享状态、对象复用等场景下往往比 channel 更直接高效。本篇我们将逐一掌握这些工具的正确用法和最佳实践。

一、sync.WaitGroup 详解

我们在第二章已经接触过 WaitGroup 的基础用法,这里做更深入的讲解。WaitGroup 用于等待一组 goroutine 完成,它内部维护一个计数器。

1. 三个核心方法

  • Add(n int):计数器加 n(正数增加,负数减少)。
  • Done():计数器减 1,等价于 Add(-1)
  • Wait():阻塞直到计数器归零。

2. 完整示例:并发处理任务并等待

go
package main

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

func main() {
	tasks := []string{"下载", "解析", "存储", "通知", "清理"}
	var wg sync.WaitGroup

	for _, task := range tasks {
		wg.Add(1) // 必须在启动 goroutine 前 Add
		go func(name string) {
			defer wg.Done() // 用 defer 确保 Done 一定执行
			fmt.Printf("开始执行: %s\n", name)
			time.Sleep(time.Second) // 模拟工作
			fmt.Printf("完成: %s\n", name)
		}(task)
	}

	wg.Wait() // 等待所有任务完成
	fmt.Println("所有任务执行完毕")
}

3. 批量 Add 优化

如果知道任务总数,可以一次性 Add,减少调用次数:

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	const N = 5
	var wg sync.WaitGroup
	wg.Add(N) // 一次性加 N

	for i := 0; i < N; i++ {
		go func(id int) {
			defer wg.Done()
			fmt.Printf("worker %d 完成\n", id)
		}(i)
	}

	wg.Wait()
	fmt.Println("全部完成")
}

4. WaitGroup 不能复制

WaitGroup 内部有状态,不能按值传递,必须用指针。这也是为什么把 WaitGroup 传给 goroutine 时要用 &wg

go
package main

import (
	"fmt"
	"sync"
)

// ✅ 正确:传指针
func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("worker", id)
}

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go worker(1, &wg) // 传指针
	wg.Wait()
}

如果把 WaitGroup 按值传给函数,每个函数拿到的是副本,Done 修改的是副本,主 goroutine 的计数器不会减——程序会死锁。Go 的 go vet 工具能检测出这种错误。

二、WaitGroup 的常见误用

1. 误用一:在 goroutine 内部 Add

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	for i := 0; i < 3; i++ {
		go func(id int) {
			wg.Add(1) // ❌ 错误:在 goroutine 内 Add
			defer wg.Done()
			fmt.Println("worker", id)
		}(i)
	}

	wg.Wait() // 可能直接返回(计数器还是 0),也可能死锁
	fmt.Println("完成")
}

问题在于 AddWait 之间存在竞态:主 goroutine 调用 Wait 时,可能 goroutine 还没来得及 Add,计数器是 0,Wait 直接返回;或者部分 Add 了部分没 Add,行为不确定。Add 必须在启动 goroutine 之前调用

2. 误用二:忘记 Done

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		// 忘记 wg.Done(),或者提前 return 跳过 Done
		fmt.Println("working")
		// 没有调用 Done
	}()

	wg.Wait() // 永远阻塞,死锁
	fmt.Println("完成")
}

defer wg.Done() 可以彻底避免这个问题——无论函数怎么退出,Done 都会执行。

3. 误用三:计数器变负数

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	// 不小心 Done 两次
	wg.Done()
	wg.Done() // 计数器变成 -1,panic: sync: negative WaitGroup counter

	wg.Wait()
	fmt.Println("完成")
}

计数器不能为负,否则 panic。这通常是因为逻辑错误导致 Done 被调用多次。

4. 误用四:Wait 期间 Add

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(1)
	go func() {
		defer wg.Done()
		wg.Add(1) // ❌ Wait 期间 Add,panic
		go func() {
			defer wg.Done()
		}()
	}()

	wg.Wait()
	fmt.Println("完成")
}

在某个 goroutine 已经 Wait 之后又 Add,会触发 panic。需要新一轮任务就新建一个 WaitGroup。

三、sync.Once:单例模式与初始化

sync.Once 保证某个操作只执行一次,即使多个 goroutine 同时调用。它是实现单例模式、懒加载、全局初始化的标准工具。

1. 基本用法

go
package main

import (
	"fmt"
	"sync"
)

var (
	once   sync.Once
	config string
)

func loadConfig() string {
	once.Do(func() {
		fmt.Println("加载配置(只执行一次)")
		config = "应用配置"
	})
	return config
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			cfg := loadConfig()
			fmt.Printf("goroutine %d 拿到配置: %s\n", id, cfg)
		}(i)
	}
	wg.Wait()
}

输出中「加载配置」只出现一次,但所有 goroutine 都拿到了配置。

2. 实现单例模式

go
package main

import (
	"fmt"
	"sync"
)

type Database struct {
	name string
}

var (
	dbInstance *Database
	dbOnce     sync.Once
)

func GetDB() *Database {
	dbOnce.Do(func() {
		fmt.Println("初始化数据库连接")
		dbInstance = &Database{name: "MySQL"}
	})
	return dbInstance
}

func main() {
	for i := 0; i < 3; i++ {
		db := GetDB()
		fmt.Printf("获取到: %s\n", db.name)
	}
}

3. Once 的注意点

  • Do 接收的函数即使 panic,Once 也会认为「已执行」,后续调用不会再执行。
  • Once 不能复制(和 WaitGroup 一样要用指针或作为结构体字段)。
  • 如果初始化逻辑可能失败,单纯用 Once 不够(失败后无法重试),需要配合其他逻辑。
go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var once sync.Once
	var wg sync.WaitGroup

	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			once.Do(func() {
				if id == 1 {
					panic("模拟初始化失败") // panic 后 once 仍认为已执行
				}
				fmt.Println("初始化成功")
			})
		}(i)
	}

	// 需要处理 panic 否则整个程序崩溃
	// 这里仅演示概念,实际要 recover
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("捕获 panic:", r)
		}
	}()
	wg.Wait()
}

四、sync.Mutex:互斥锁

sync.Mutex 是最基本的互斥锁,用来保护临界区——同一时刻只允许一个 goroutine 访问某段代码或数据。

1. 基本用法

go
package main

import (
	"fmt"
	"sync"
)

type Counter struct {
	mu    sync.Mutex
	count int
}

func (c *Counter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock() // 用 defer 确保解锁
	c.count++
}

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

func main() {
	c := &Counter{}
	var wg sync.WaitGroup

	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			c.Inc()
		}()
	}

	wg.Wait()
	fmt.Println("最终计数:", c.Value()) // 1000
}

2. 为什么需要锁

如果不加锁,1000 个 goroutine 同时 c.count++ 会发生数据竞争——++ 不是原子操作(读-改-写三步),最终结果会小于 1000。加锁后保证每次自增完整执行。

3. 锁的正确姿势

  • Lock 后立即 defer Unlock:这是最安全的模式,避免任何分支忘记解锁。
  • 不要在持锁时做耗时操作:如 I/O、网络请求、长时间计算,会严重降低并发度。
  • 锁不要嵌套:嵌套锁容易死锁,如果必须嵌套要仔细分析顺序。
go
package main

import (
	"fmt"
	"sync"
)

type SafeMap struct {
	mu   sync.Mutex
	data map[string]int
}

func NewSafeMap() *SafeMap {
	return &SafeMap{data: make(map[string]int)}
}

func (m *SafeMap) Set(key string, val int) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.data[key] = val
}

func (m *SafeMap) Get(key string) (int, bool) {
	m.mu.Lock()
	defer m.mu.Unlock()
	v, ok := m.data[key]
	return v, ok
}

func (m *SafeMap) Delete(key string) {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.data, key)
}

func main() {
	m := NewSafeMap()
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			key := fmt.Sprintf("k%d", i)
			m.Set(key, i)
		}(i)
	}
	wg.Wait()

	fmt.Println("map 大小:", len(m.data))
	v, ok := m.Get("k50")
	fmt.Println("k50:", v, ok)
}

4. Mutex 不可重入

Go 的 Mutex 不是可重入的——同一个 goroutine 二次 Lock 会死锁:

go
package main

import (
	"fmt"
	"sync"
)

type Service struct {
	mu sync.Mutex
}

func (s *Service) Outer() {
	s.mu.Lock()
	defer s.mu.Unlock()
	fmt.Println("Outer 持有锁")
	s.Inner() // ❌ Inner 又会 Lock,但当前 goroutine 已持锁,死锁
}

func (s *Service) Inner() {
	s.mu.Lock()
	defer s.mu.Unlock()
	fmt.Println("Inner")
}

func main() {
	s := &Service{}
	s.Outer()
}

这与 Java 的 synchronized(可重入)不同。Go 的设计哲学是:可重入锁容易隐藏设计问题,应该重构代码避免在持锁时调用其他需要锁的方法。

五、sync.RWMutex:读写锁

sync.RWMutex 是读写锁:允许多个读操作并行,但写操作独占。适合读多写少的场景。

1. 基本用法

go
package main

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

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

func NewCache() *Cache {
	return &Cache{data: make(map[string]string)}
}

func (c *Cache) Get(key string) (string, bool) {
	c.mu.RLock()         // 读锁
	defer c.mu.RUnlock() // 释放读锁
	v, ok := c.data[key]
	return v, ok
}

func (c *Cache) Set(key, val string) {
	c.mu.Lock()         // 写锁
	defer c.mu.Unlock() // 释放写锁
	c.data[key] = val
}

func main() {
	cache := NewCache()
	cache.Set("name", "Go")

	var wg sync.WaitGroup

	// 多个并发读
	for i := 0; i < 5; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < 3; j++ {
				if v, ok := cache.Get("name"); ok {
					fmt.Printf("reader %d: %s\n", id, v)
				}
				time.Sleep(100 * time.Millisecond)
			}
		}(i)
	}

	// 偶尔写
	for i := 0; i < 2; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			cache.Set("name", fmt.Sprintf("Go-%d", id))
			fmt.Printf("writer %d 写入\n", id)
		}(i)
	}

	wg.Wait()
	fmt.Println("最终 name:", func() string {
		v, _ := cache.Get("name")
		return v
	}())
}

2. RWMutex 的规则

  • 多个 RLock 可以同时持有(并发读)。
  • Lock 是独占的,持有写锁时其他读锁和写锁都要等待。
  • 写锁会等待所有已持有的读锁释放后才能获取。
  • 写锁等待期间,新的读锁也会被阻塞(防止写饥饿)。

3. 何时用 RWMutex

  • 读远多于写:比如缓存、配置,RWMutex 比 Mutex 并发度更高。
  • 读写相当或写多读少:RWMutex 的开销(维护读者计数)反而比 Mutex 大,直接用 Mutex 更好。
  • 临界区很短:RWMutex 优势不明显,Mutex 更简单。

实测中,临界区很短时 Mutex 往往比 RWMutex 快,因为 RWMutex 的元数据维护成本更高。不要盲目用 RWMutex,要 benchmark。

六、sync.Cond:条件变量

sync.Cond 是条件变量,用于 goroutine 之间「等待某个条件成立」。它通常和 Mutex 配合使用:goroutine 持锁检查条件,条件不满足就 Wait(自动释放锁并阻塞),其他 goroutine 改变条件后 SignalBroadcast 唤醒等待者。

1. 基本示例:生产者-消费者

go
package main

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

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

func NewQueue(max int) *Queue {
	q := &Queue{maxLen: max}
	q.cond = sync.NewCond(&q.mu) // 关联一个 Mutex
	return q
}

func (q *Queue) Put(item int) {
	q.mu.Lock()
	defer q.mu.Unlock()
	for len(q.items) >= q.maxLen { // 用 for 防止虚假唤醒
		q.cond.Wait() // 释放锁并等待,被唤醒时重新获取锁
	}
	q.items = append(q.items, item)
	q.cond.Signal() // 通知一个等待的消费者
}

func (q *Queue) Get() int {
	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
}

func main() {
	q := NewQueue(3)
	var wg sync.WaitGroup

	// 生产者
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 1; i <= 10; i++ {
			q.Put(i)
			fmt.Printf("生产 %d\n", i)
			time.Sleep(100 * time.Millisecond)
		}
	}()

	// 消费者
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			item := q.Get()
			fmt.Printf("  消费 %d\n", item)
			time.Sleep(200 * time.Millisecond)
		}
	}()

	wg.Wait()
	fmt.Println("完成")
}

2. Signal vs Broadcast

  • Signal():唤醒一个等待的 goroutine。
  • Broadcast():唤醒所有等待的 goroutine。

通常单个资源可用用 Signal,状态变化影响所有等待者用 Broadcast。

3. Cond 的使用要点

  • Wait 必须在持锁状态下调用,它会释放锁、阻塞、被唤醒后重新获取锁。
  • 条件检查必须用 for 循环,不能用 if——因为可能被虚假唤醒。
  • Cond 在 Go 中用得相对少,因为很多场景 channel 更直观。但在「等待条件满足」这类场景,Cond 比 channel 更合适。

七、sync.Map:并发安全 Map

普通 map 不是并发安全的,多个 goroutine 同时读写会 panic。sync.Map 是 Go 提供的并发安全 Map,适合读多写少、key 相对稳定的场景。

1. 基本用法

go
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	// 存储
	m.Store("name", "Go")
	m.Store("version", "1.22")

	// 读取
	v, ok := m.Load("name")
	fmt.Println("name:", v, ok) // Go true

	v, ok = m.Load("age")
	fmt.Println("age:", v, ok) // <nil> false

	// 不存在则存储
	actual, loaded := m.LoadOrStore("name", "Rust")
	fmt.Println("name:", actual, "已存在:", loaded) // Go true

	// 删除
	m.Delete("version")
	v, ok = m.Load("version")
	fmt.Println("version:", v, ok) // <nil> false

	// 遍历
	m.Range(func(key, value any) bool {
		fmt.Printf("遍历: %v = %v\n", key, value)
		return true
	})
}

2. sync.Map vs Mutex+Map

特性sync.MapMutex + map
读多写少优(读无锁)一般(每次读要锁)
写多一般
key 稳定优(内部缓存 key)一般
类型安全弱(用 any)强(泛型/具体类型)
API 友好度较繁琐简洁

经验法则:

  • 读多写少 + key 集合稳定:用 sync.Map。
  • 写多或 key 频繁变化:用 Mutex + 普通 map。
  • 需要类型安全:用 Mutex + 泛型 map(Go 1.18+)。

3. Go 1.24 的泛型同步 map

Go 1.24 引入了 sync.Map 的泛型版本(实际上叫 synctesting 或类似,具体看版本)。更通用的做法是自封装:

go
package main

import (
	"fmt"
	"sync"
)

// 泛型并发安全 map
type ConcurrentMap[K comparable, V any] struct {
	mu   sync.RWMutex
	data map[K]V
}

func NewConcurrentMap[K comparable, V any]() *ConcurrentMap[K, V] {
	return &ConcurrentMap[K, V]{data: make(map[K]V)}
}

func (m *ConcurrentMap[K, V]) Set(k K, v V) {
	m.mu.Lock()
	defer m.mu.Unlock()
	m.data[k] = v
}

func (m *ConcurrentMap[K, V]) Get(k K) (V, bool) {
	m.mu.RLock()
	defer m.mu.RUnlock()
	v, ok := m.data[k]
	return v, ok
}

func (m *ConcurrentMap[K, V]) Delete(k K) {
	m.mu.Lock()
	defer m.mu.Unlock()
	delete(m.data, k)
}

func main() {
	m := NewConcurrentMap[string, int]()
	m.Set("a", 1)
	m.Set("b", 2)

	v, ok := m.Get("a")
	fmt.Println("a =", v, ok) // 1 true

	v, ok = m.Get("c")
	fmt.Println("c =", v, ok) // 0 false
}

八、sync.Pool:对象复用

sync.Pool 是对象池,用于复用临时对象,减少 GC 压力。典型场景:复用 bytes.Bufferjson.Encoder 等临时对象。

1. 基本用法

go
package main

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

var bufPool = sync.Pool{
	New: func() any {
		fmt.Println("创建新 buffer")
		return new(bytes.Buffer)
	},
}

func process(data string) string {
	// 从池中借一个 buffer
	buf := bufPool.Get().(*bytes.Buffer)
	defer func() {
		buf.Reset()             // 重置以便复用
		bufPool.Put(buf)        // 还回池中
	}()

	buf.WriteString("prefix-")
	buf.WriteString(data)
	return buf.String()
}

func main() {
	for i := 0; i < 5; i++ {
		result := process(fmt.Sprintf("data%d", i))
		fmt.Println(result)
	}
}

「创建新 buffer」只在第一次(或被 GC 回收后)打印,后续都复用已有对象。

2. Pool 的特点

  • Get 可能返回新对象(池为空时调用 New)。
  • Put 后对象可能被随时回收:Pool 中的对象会在 GC 时被清理,所以 Pool 不保证对象一直存在。
  • 不适合做连接池:因为对象可能被 GC 清掉,连接池应该用专门的实现。
  • 适合短生命周期、高频创建的对象

3. 实战:复用 bytes.Buffer

go
package main

import (
	"bytes"
	"encoding/json"
	"fmt"
	"sync"
)

var bufferPool = sync.Pool{
	New: func() any {
		return bytes.NewBuffer(make([]byte, 0, 1024))
	},
}

func toJSON(v any) (string, error) {
	buf := bufferPool.Get().(*bytes.Buffer)
	defer func() {
		buf.Reset()
		bufferPool.Put(buf)
	}()

	encoder := json.NewEncoder(buf)
	if err := encoder.Encode(v); err != nil {
		return "", err
	}
	// Encode 会加一个换行,去掉
	s := buf.String()
	if len(s) > 0 && s[len(s)-1] == '\n' {
		s = s[:len(s)-1]
	}
	return s, nil
}

func main() {
	for i := 0; i < 3; i++ {
		data := map[string]int{"id": i, "score": i * 10}
		s, _ := toJSON(data)
		fmt.Println(s)
	}
}

在 JSON 序列化、字符串拼接等高频场景,用 Pool 复用 Buffer 能显著减少内存分配和 GC 压力。

九、锁的最佳实践

1. 减小临界区

只锁真正需要保护的部分,不要把无关代码也放进锁里:

go
package main

import (
	"fmt"
	"strings"
	"sync"
)

type Logger struct {
	mu     sync.Mutex
	logs   []string
}

// ✅ 好:先做耗时操作,再持锁写入
func (l *Logger) Log(msg string) {
	formatted := strings.ToUpper(msg) // 耗时操作放锁外
	l.mu.Lock()
	l.logs = append(l.logs, formatted)
	l.mu.Unlock()
}

func main() {
	logger := &Logger{}
	var wg sync.WaitGroup
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			logger.Log(fmt.Sprintf("event %d", i))
		}(i)
	}
	wg.Wait()
	fmt.Println("日志数:", len(logger.logs))
}

2. 避免锁嵌套

go
package main

import "sync"

type A struct {
	mu sync.Mutex
}

type B struct {
	mu sync.Mutex
	a  *A
}

// ❌ 危险:先锁 B 再锁 A,如果别处先锁 A 再锁 B,死锁
func (b *B) BadMethod() {
	b.mu.Lock()
	defer b.mu.Unlock()
	b.a.mu.Lock() // 嵌套锁
	defer b.a.mu.Unlock()
}

// ✅ 好:拆分,先获取 A 的数据再操作
func main() {}

如果必须用多把锁,要保证全程序获取锁的顺序一致(比如总是先 A 后 B)。

3. 用 defer Unlock 防止遗漏

go
package main

import "sync"

type SafeCounter struct {
	mu sync.Mutex
	n  int
}

func (c *SafeCounter) Inc() {
	c.mu.Lock()
	defer c.mu.Unlock()
	c.n++
}

4. 不要持锁做 I/O

I/O 涉及系统调用和阻塞,持锁期间会卡住所有等待该锁的 goroutine。应该先在锁内拷贝数据,释放锁后再做 I/O。

十、Mutex vs Channel:何时用锁何时用 channel

这是 Go 开发者常问的问题。Go 官方的建议是:

不要默认认为一种比另一种好。它们是互补的工具。

1. 适合用 channel 的场景

  • 传递数据所有权:一个 goroutine 把数据交给另一个。
  • 协调多个 goroutine:通知、等待、扇入扇出。
  • 并发任务的结果收集:worker 把结果发到 channel。
  • 需要明确的生命周期:channel 的关闭天然表达「结束」。
go
package main

import "fmt"

// channel 适合:传递数据、协调
func main() {
	results := make(chan int)
	go func() {
		// 计算
		results <- 42
	}()
	fmt.Println("结果:", <-results)
}

2. 适合用锁的场景

  • 保护共享状态:缓存、计数器、配置。
  • 简单的读写保护:不需要复杂协调,就是「别同时改」。
  • 性能敏感的临界区:锁的开销通常比 channel 小。
go
package main

import (
	"fmt"
	"sync"
)

// 锁适合:保护共享状态
type Counter struct {
	mu sync.Mutex
	n  int
}

func (c *Counter) Inc() {
	c.mu.Lock()
	c.mu.Unlock()
	c.n++
}

func main() {
	c := &Counter{}
	c.Inc()
	fmt.Println(c.n)
}

3. 选择建议

  • 如果是「传递数据」用 channel,如果是「保护数据」用锁。
  • 不确定时,先用最直观的方式实现,再根据需要优化。
  • 不要为了用 channel 而把简单的状态保护搞复杂。

十一、小结

本篇我们系统学习了 sync 包的同步原语:

  1. WaitGroup:等待一组 goroutine 完成。Add 必须在 goroutine 启动前调用,Done 用 defer,不能复制(用指针),不能在 Wait 期间 Add。

  2. Once:保证操作只执行一次,用于单例和初始化。panic 后也认为已执行,不能复制。

  3. Mutex:互斥锁,保护临界区。Lock 后立即 defer Unlock,不可重入(二次 Lock 会死锁),不要在持锁时做耗时操作。

  4. RWMutex:读写锁,读多写少时提高并发度。但临界区短时 Mutex 可能更快,要 benchmark。

  5. Cond:条件变量,等待条件成立。Wait 必须持锁,条件检查用 for 循环防虚假唤醒。Signal 唤醒一个,Broadcast 唤醒全部。

  6. Map:并发安全 Map,适合读多写少、key 稳定场景。写多时用 Mutex+map 更好。Go 1.18+ 可用泛型自封装类型安全的并发 map。

  7. Pool:对象复用,减少 GC 压力。对象可能在 GC 时被清理,不适合做连接池。适合 bytes.Buffer 等短命高频对象。

  8. 最佳实践:减小临界区、避免锁嵌套、用 defer Unlock、不要持锁做 I/O。

  9. Mutex vs Channel:传递数据用 channel,保护状态用锁。它们是互补的,不要教条地只用一种。

下一篇我们将把这些工具组合起来,学习经典并发模式:Worker Pool、Pipeline、Fan-out/Fan-in 等。