Go 的 sync 包用于并发协调而非开启并发,WaitGroup 需正确调用 Add/Wait/Done,Mutex/RWMutex 保护临界区而非变量本身,Once 保证单次执行但不重试失败,Pool 仅适用于无状态临时对象。
Go 的 sync 包不是用来“开启并发”的,而是用来在已有 goroutine 并发场景下防止数据竞争、协调执行顺序。用错地方(比如想靠它限制 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 完成
误以为加了 Mutex 就能“让代码变线程安全”——其实只对被 Lock()/Unlock() 包裹的那段临界区生效;变量本身没魔法,保护的是访问路径。
RWMutex:RLock()/RUnlock() 允许多个 goroutine 同时读,Lock()/Unlock() 写时独占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
}
典型误用是拿它当“懒加载单例”的唯一手段,却忽略了它不处理初始化失败重试,也不提供错误反馈机制。
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 }
很多人把它当成通用对象缓存池,结果发现对象被意外回收、状态丢失、甚至内存不降反升。
Pool 中的对象可能在任意 GC 周期被清理,不保证存活时间;不能依赖它维持连接、事务上下文、用户 session 等有状态对象[]byte、bytes.Buffer),且必须实现 New 函数来兜底创建新实例Get() 返回的对象曾被用过,务必在复用前清空内部状态(比如 buf.Reset()),否则残留数据会导致 bugvar 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 或 channel;要跨 goroutine 传值,sync.Map 不如 context 清晰。工具只是补丁,设计才是根本。