目录

Go Sync 包

Go 的 sync 包提供了基本的同步原语,用于在 goroutine 之间协调对共享资源的访问。除了互斥锁和读写锁,还包括 WaitGroupOnceMapCond 等实用工具。配合 sync/atomic 包的原子操作,可以构建高效的并发程序。

https://img.zhaojq.top/20260729163101002.png
同步机制对比

sync.WaitGroup

WaitGroup 用于等待一组 goroutine 完成。它维护一个内部计数器,Add 增加、Done 减少,Wait 阻塞直到计数器归零。

package main

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

func fetchPage(url string, wg *sync.WaitGroup) {
	defer wg.Done()

	fmt.Printf("开始抓取: %s\n", url)
	time.Sleep(200 * time.Millisecond) // 模拟网络请求
	fmt.Printf("完成抓取: %s\n", url)
}

func main() {
	urls := []string{
		"https://example.com",
		"https://golang.org",
		"https://github.com",
	}

	var wg sync.WaitGroup

	for _, url := range urls {
		wg.Add(1)
		go fetchPage(url, &wg)
	}

	wg.Wait()
	fmt.Println("所有页面抓取完成")
}

使用要点:Add(1) 必须在 go 语句之前调用,Done() 通常用 defer 确保执行。WaitGroup 不能复制,应传递指针。

sync.Mutex(互斥锁)

Mutex 是最基本的锁机制,保证同一时刻只有一个 goroutine 能访问受保护的代码段。

package main

import (
	"fmt"
	"sync"
)

// SafeCounter 使用 Mutex 保护计数器
type SafeCounter struct {
	mu    sync.Mutex
	count int
}

func (c *SafeCounter) Increment() {
	c.mu.Lock()
	defer c.mu.Unlock() // 确保 Unlock 一定被调用
	c.count++
}

func (c *SafeCounter) Get() int {
	c.mu.Lock()
	defer c.mu.Unlock()
	return c.count
}

func main() {
	counter := &SafeCounter{}
	var wg sync.WaitGroup

	// 启动 1000 个 goroutine 并发递增
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			counter.Increment()
		}()
	}

	wg.Wait()
	fmt.Printf("最终计数: %d\n", counter.Get()) // 1000
}

Lock() 获取锁,如果锁已被其他 goroutine 持有则阻塞。Unlock() 释放锁。通常使用 defer c.mu.Unlock() 确保锁在函数返回时释放。

sync.RWMutex(读写锁)

RWMutex 允许多个读操作并发执行,但写操作独占。适合读多写少的场景。

package main

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

// SafeMap 使用 RWMutex 保护 map
type SafeMap struct {
	mu   sync.RWMutex
	data map[string]int
}

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

func (m *SafeMap) Get(key string) (int, bool) {
	m.mu.RLock()         // 读锁:允许多个并发读
	defer m.mu.RUnlock()
	val, ok := m.data[key]
	return val, ok
}

func (m *SafeMap) Set(key string, value int) {
	m.mu.Lock()          // 写锁:独占
	defer m.mu.Unlock()
	m.data[key] = value
}

func main() {
	sm := NewSafeMap()
	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 val, ok := sm.Get(fmt.Sprintf("key-%d", j)); ok {
					fmt.Printf("Reader %d 读取 key-%d = %d\n", id, j, val)
				}
			}
		}(i)
	}

	// 写操作
	for i := 0; i < 3; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			sm.Set(fmt.Sprintf("key-%d", id), id*100)
			fmt.Printf("Writer %d 写入 key-%d = %d\n", id, id, id*100)
		}(i)
	}

	time.Sleep(100 * time.Millisecond)
	wg.Wait()

	// 验证结果
	for i := 0; i < 3; i++ {
		key := fmt.Sprintf("key-%d", i)
		val, _ := sm.Get(key)
		fmt.Printf("%s = %d\n", key, val)
	}
}

RWMutex 的方法:Lock()/Unlock() 用于写操作,RLock()/RUnlock() 用于读操作。当有写锁持有时,新的读锁请求会阻塞;当有读锁持有时,写锁请求也会阻塞。

sync.Once

Once 确保某个操作只执行一次,即使多个 goroutine 同时调用。

package main

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

var (
	instance *Config
	once     sync.Once
)

type Config struct {
	DatabaseURL string
	APIKey      string
}

func GetConfig() *Config {
	once.Do(func() {
		fmt.Println("初始化配置(只执行一次)...")
		time.Sleep(100 * time.Millisecond) // 模拟初始化
		instance = &Config{
			DatabaseURL: "postgres://localhost:5432/mydb",
			APIKey:      "sk-xxxx",
		}
	})
	return instance
}

func main() {
	var wg sync.WaitGroup

	// 10 个 goroutine 同时请求配置
	for i := 0; i < 10; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			cfg := GetConfig()
			fmt.Printf("Goroutine %d 获取配置: %s\n", id, cfg.DatabaseURL)
		}(i)
	}

	wg.Wait()
	fmt.Println("所有 goroutine 获取到相同的配置实例")
}

Once.Do(fn) 保证 fn 只被执行一次。即使多个 goroutine 同时调用 Do,也只有一个会执行 fn,其他 goroutine 会阻塞直到 fn 完成。常用于单例模式和延迟初始化。

sync.Map

sync.Map 是并发安全的 map 实现,针对两种场景优化:

  1. 键只写入一次,但多次读取(缓存)
  2. 多个 goroutine 读写不同的键
package main

import (
	"fmt"
	"sync"
)

func main() {
	var m sync.Map

	// 存储
	m.Store("name", "张三")
	m.Store("age", 28)
	m.Store("city", "北京")

	// 读取
	if val, ok := m.Load("name"); ok {
		fmt.Printf("name = %v\n", val)
	}

	// LoadOrStore:不存在时存储,存在时返回已有值
	actual, loaded := m.LoadOrStore("name", "李四")
	fmt.Printf("name = %v, 是否已有: %v\n", actual, loaded)

	// LoadAndDelete:读取并删除
	val, deleted := m.LoadAndDelete("city")
	fmt.Printf("删除 city = %v, 是否成功: %v\n", val, deleted)

	// 遍历
	fmt.Println("\n所有键值对:")
	m.Range(func(key, value interface{}) bool {
		fmt.Printf("  %v = %v\n", key, value)
		return true // 返回 false 停止遍历
	})

	// 并发安全示例
	var wg sync.WaitGroup
	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			key := fmt.Sprintf("key-%d", n%10) // 10 个不同的键
			m.Store(key, n)
		}(i)
	}
	wg.Wait()
	fmt.Println("\n并发写入完成")
}

sync.Map 不需要加锁,但在需要频繁遍历或键集变化大的场景下,Mutex + map 可能更合适。

sync.Cond(条件变量)

Cond 实现条件变量,允许 goroutine 等待某个条件成立。

package main

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

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

func NewQueue() *Queue {
	q := &Queue{}
	q.cond = sync.NewCond(&q.mu)
	return q
}

func (q *Queue) Push(item int) {
	q.mu.Lock()
	defer q.mu.Unlock()
	q.items = append(q.items, item)
	q.cond.Signal() // 唤醒一个等待的 goroutine
}

func (q *Queue) Pop() 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:]
	return item
}

func main() {
	q := NewQueue()

	// 消费者
	go func() {
		for i := 0; i < 5; i++ {
			val := q.Pop()
			fmt.Printf("消费: %d\n", val)
			time.Sleep(100 * time.Millisecond)
		}
	}()

	// 生产者
	for i := 1; i <= 5; i++ {
		time.Sleep(50 * time.Millisecond)
		fmt.Printf("生产: %d\n", i)
		q.Push(i)
	}

	time.Sleep(600 * time.Millisecond)
}

Cond.Wait() 会释放锁并阻塞,被 Signal()Broadcast() 唤醒后重新获取锁。始终在循环中检查条件,因为唤醒可能是虚假的。

atomic 包

sync/atomic 提供原子操作,无需加锁即可安全地操作基本类型。

package main

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

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

	// 原子递增
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			atomic.AddInt64(&counter, 1)
		}()
	}
	wg.Wait()
	fmt.Printf("原子计数: %d\n", atomic.LoadInt64(&counter)) // 1000

	// 原子存储和加载
	var status int64
	atomic.StoreInt64(&status, 1)
	fmt.Printf("状态: %d\n", atomic.LoadInt64(&status))

	// CompareAndSwap(CAS)
	var enabled int64 = 0
	swapped := atomic.CompareAndSwapInt64(&enabled, 0, 1)
	fmt.Printf("CAS 成功: %v, 新值: %d\n", swapped, enabled)

	// CAS 失败的情况
	swapped2 := atomic.CompareAndSwapInt64(&enabled, 0, 2) // 当前值是 1,不是 0
	fmt.Printf("CAS 失败: %v, 值不变: %d\n", swapped2, enabled)

	// 使用 atomic.Value 存储任意类型
	var config atomic.Value
	config.Store(map[string]string{"env": "production"})

	cfg := config.Load().(map[string]string)
	fmt.Printf("配置: %v\n", cfg)
}

原子操作比 Mutex 更轻量,适合简单的计数器和标志位。atomic.Value 可以原子地存储和加载任意类型的值。

同步工具对比

工具 适用场景 特点
WaitGroup 等待多个 goroutine 完成 简单计数,不保护数据
Mutex 保护共享资源的读写 互斥,任何时刻只有一个 goroutine 持有锁
RWMutex 读多写少的共享数据 多读并发,写独占
Once 单例初始化 保证只执行一次
Map 并发 map(特定场景) 无需加锁,适合缓存
Cond 等待条件成立 配合锁使用,支持 Signal/Broadcast
atomic 简单的计数器/标志位 无锁操作,性能最高

总结

sync 包提供了 Go 并发编程所需的核心同步工具。WaitGroup 等待一组 goroutine 完成;MutexRWMutex 保护共享数据,后者在读多写少时更高效;Once 保证初始化只执行一次;sync.Map 为特定场景提供无锁并发 map;Cond 实现条件等待;atomic 包提供无锁的原子操作。选择合适的同步工具是编写高效并发程序的关键——简单计数用 atomic,保护复杂数据用 Mutex/RWMutex,等待完成用 WaitGroup