sync.waitgroup 用于等待一组 goroutine 完成,必须在 goroutine 启动前调用 add,done 应 defer 调用,不可复制、不可重入,否则会 panic。

sync.WaitGroup 用在哪、怎么用才不 panic
WaitGroup 主要用于等待一组 goroutine 完成,而不是控制临界区访问。它在微服务中常用于启动多个后台任务(如健康检查上报、日志 flush、配置热加载)后统一等待退出。
常见错误是 Add 调用时机不对:必须在 go 启动前调用,否则 Wait 可能提前返回,导致主 goroutine 退出而子 goroutine 还在跑。
-
Add必须在 goroutine 启动前执行;不能在 goroutine 内部调用Add -
Done推荐用defer wg.Done(),确保所有返回路径都触发 - 计数器不能为负,多次
Done会导致 panic;Wait不可重入,重复调用会 panic - WaitGroup 实例不能被复制(它是含指针字段的 struct),应传指针或作为包级变量
sync.Mutex 和 sync.RWMutex 性能与选型差异
Mutex 是最常用的互斥锁,适合读写频率相近或写操作频繁的场景;RWMutex 在读多写少时更高效,但写操作会阻塞所有读,且额外开销略大。
微服务中典型用法是保护配置缓存、连接池状态、指标计数器等共享内存结构。注意 RWMutex 的 RUnlock 必须和 RLock 成对,漏掉一个就会永久阻塞后续写操作。
- 不要用
RWMutex保护只写不读的变量——它比Mutex更重 - 避免在持有
RWMutex.RLock()时调用可能阻塞或递归获取同一锁的函数 - Go 1.9+ 的 Mutex 饥饿模式默认开启,能缓解“写饥饿”,但不会完全消除;高并发写场景仍建议拆分锁粒度
- 锁的生命周期应尽量短,别在锁内做 HTTP 请求、数据库查询等耗时操作
sync.Once 确保初始化只执行一次的边界情况
sync.Once 常用于微服务中单例资源初始化,比如全局 HTTP client、gRPC 连接、logger 实例。它的作用不是线程安全地读写变量,而是保证某个函数最多执行一次。
关键点在于:一旦 Do 中的函数 panic,Once 会认为已执行完成,后续调用不再尝试——这容易被忽略,导致初始化失败却无报错。
-
Do的函数参数必须是func()类型,不能带返回值或 error - 如果初始化逻辑可能失败,应在函数内部处理 panic 或用额外标志位 + 错误缓存,而不是依赖
Once自动重试 -
Once不提供同步读能力,初始化后的变量仍需按需加锁或用 atomic 访问 - 多个
Once实例之间无顺序保证,有依赖关系的初始化需手动串行化
channel 和 sync 包原语混用时最容易踩的坑
微服务里经常同时用 channel 做通信、sync 原语做状态保护,但两者混合时容易出现死锁或竞态。比如用 channel 通知“数据已就绪”,却在接收端用 Mutex 保护写入,而发送端没加锁——这时读写就可能错乱。
典型陷阱是把 channel 当作锁用:make(chan struct{}, 1) 确实能实现类似互斥效果,但它不具备锁的语义(比如不可重入、无所有权概念),且阻塞行为和 goroutine 调度耦合更深,调试困难。
- 不要用无缓冲 channel 替代
Mutex保护共享状态;它无法防止多个 goroutine 同时读取未加锁的变量 - 在 select 语句中使用
case 退出时,记得检查是否已释放锁,否则可能造成 goroutine 持有锁永久阻塞 - WaitGroup + channel 组合时,确保所有 goroutine 都调用了
Done,哪怕因 channel 关闭提前退出 - Context cancel 信号到达后,应尽快释放持有的 sync 锁,避免阻塞 cleanup 流程
sync.RWMutex 保护的 map,被多个 goroutine 通过 channel 发送 key 来读写,这时候锁的范围、channel 的关闭时机、以及 panic 恢复的位置,任何一个出错都会让整个服务卡住。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











