如何使用Golang sync包实现并发控制_Golang并发工具使用说明

Go 的 sync 包用于并发协调而非开启并发,WaitGroup 需正确调用 Add/Wait/Done,Mutex/RWMutex 保护临界区而非变量本身,Once 保证单次执行但不重试失败,Pool 仅适用于无状态临时对象。

Go 的 sync 包不是用来“开启并发”的,而是用来在已有 goroutine 并发场景下防止数据竞争、协调执行顺序。用错地方(比如想靠它限制 goroutine 启动数量)会白忙活。

sync.WaitGroup 适合等一组 goroutine 全部结束

常见错误是 WaitGroup.Add() 调用时机不对,导致 Wait() 提前返回或 panic;或者在 goroutine 内部漏掉 Done()

  • Add() 必须在启动 goroutine 之前调用,且不能在 goroutine 内部调用(除非你明确加锁)
  • 每个 go 启动的函数里,必须有且仅有一次 Done(),建议用 defer wg.Done()
  • 不要复用已 Wait() 过的 WaitGroup,它不支持重置;需要重复使用请新建实例
var wg sync.WaitGroup
for _, url := range urls {
    wg.Add(1)
    go func(u string) {
        defer wg.Done()
        fetch(u) // 实际处理逻辑
    }(url)
}
wg.Wait() // 阻塞直到所有 fetch 完成

sync.Mutex 和 sync.RWMutex 控制对共享变量的读写访问

误以为加了 Mutex 就能“让代码变线程安全”——其实只对被 Lock()/Unlock() 包裹的那段临界区生效;变量本身没魔法,保护的是访问路径。

  • 读多写少场景优先用 RWMutexRLock()/RUnlock() 允许多个 goroutine 同时读,Lock()/Unlock() 写时独占
  • 别把锁对象作为参数传进 goroutine,容易造成锁状态混乱;应捕获外层锁变量的引用,或确保锁生命周期覆盖整个临界区
  • 避免死锁:固定加锁顺序(如按字段名排序)、使用 defer mu.Unlock()、不跨函数传递未解锁的锁
var mu sync.RWMutex
var cache = make(map[string]string)

func Get(key string) string {
    mu.RLock()
    defer mu.RUnlock()
    return cache[key]
}

func Set(key, value string) {
    mu.Lock()
    defer mu.Unlock()
    cache[key] = value
}

sync.Once 保证某个操作仅执行一次,但不阻塞后续调用

典型误用是拿它当“懒加载单例”的唯一手段,却忽略了它不处理初始化失败重试,也不提供错误反馈机制。

  • Once.Do() 内部函数若 panic,Once 会记录为“已执行”,后续调用直接返回,不会重试
  • 需要错误处理或重试逻辑,得自己包装一层(比如配合 sync.Mutex + 显式标志位)
  • 适用于无副作用、幂等、确定成功概率极高的初始化,比如注册信号处理器、打开只读配置文件
var loadConfigOnce sync.Once
var config map[string]string

func LoadConfig() map[string]string {
    loadConfigOnce.Do(func() {
        config = readConfigFromFile() // 假设这个函数不会 panic
    })
    return config
}

sync.Pool 不适合保存长生命周期对象或带状态的资源

很多人把它当成通用对象缓存池,结果发现对象被意外回收、状态丢失、甚至内存不降反升。

  • Pool 中的对象可能在任意 GC 周期被清理,不保证存活时间;不能依赖它维持连接、事务上下文、用户 session 等有状态对象
  • 适合缓存临时分配的小对象(如 []bytebytes.Buffer),且必须实现 New 函数来兜底创建新实例
  • 如果 Get() 返回的对象曾被用过,务必在复用前清空内部状态(比如 buf.Reset()),否则残留数据会导致 bug
var bufPool = sync.Pool{
    New: func() interface{} {
        return new(bytes.Buffer)
    },
}

func process(data []byte) {
    buf := bufPool.Get().(*bytes.Buffer)
    defer func() {
        buf.Reset() // 关键:清空内容再放回
        bufPool.Put(buf)
    }()
    buf.Write(data)
    // ... 处理逻辑
}

真正难的不是记住这些类型的 API,而是判断「此刻该不该用它们」——比如要限流,sync.WaitGroup 没用,得上 semaphore 或 channe

l;要跨 goroutine 传值,sync.Map 不如 context 清晰。工具只是补丁,设计才是根本。