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() 包裹的那段临界区生效;变量本身没魔法,保护的是访问路径。
- 读多写少场景优先用
RWMutex:RLock()/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 保证某个操作仅执行一次,但不阻塞后续调用
典型误用是拿它当“懒加载单例”的唯一手段,却忽略了它不处理初始化失败重试,也不提供错误反馈机制。
立即学习“go语言免费学习笔记(深入)”;
-
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 等有状态对象 - 适合缓存临时分配的小对象(如
[]byte、bytes.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 或 channel;要跨 goroutine 传值,sync.Map 不如 context 清晰。工具只是补丁,设计才是根本。










