Go标准库未提供sync.Barrier,推荐用sync.WaitGroup实现一次性栅栏,或用sync.Mutex+sync.Cond封装可重用Barrier;需避免竞态、死锁及Cond误用,且Barrier本身不保证数据安全。

Go 里没有内置 sync.Barrier,得自己造
Go 标准库确实没提供 Barrier 类型——不像 Java 的 CyclicBarrier 或 Python 的 threading.Barrier。这不是遗漏,而是 Go 更倾向用 channel + sync.WaitGroup 组合表达“所有协程到齐再一起往下走”这个语义。硬套其他语言的 Barrier 模式,反而容易写出阻塞死锁或竞态代码。
常见错误是试图用多个 sync.Mutex + cond.Wait() 模拟,结果漏了广播、忘了重置、或在非持有锁时调用 Wait(),直接 panic:sync: cond.Wait with uninitialized Cond。
实操建议:
- 优先用
sync.WaitGroup+ 一个闭包控制“放行点”,简单可靠 - 若需可重用(比如多轮齐步走),才考虑封装成结构体,内部用
sync.Mutex+sync.Cond - 别用
time.Sleep或轮询“等别人”,这是反模式,浪费 CPU 且不准
用 sync.WaitGroup 实现一次性栅栏最稳妥
适合只跑一轮的场景,比如初始化阶段:10 个 goroutine 各自加载配置,全部完成后再启动主逻辑。核心思路是让每个协程在到达栅栏时调用 wg.Done(),主协程用 wg.Wait() 阻塞,等全员抵达。
立即学习“go语言免费学习笔记(深入)”;
注意:WaitGroup 的 Add() 必须在 goroutine 启动前调用,否则可能 Done() 先于 Add() 导致 panic:sync: negative WaitGroup counter。
示例片段:
var wg sync.WaitGroup
wg.Add(3)
go func() { defer wg.Done(); loadDB() }()
go func() { defer wg.Done(); loadCache() }()
go func() { defer wg.Done(); loadConfig() }()
wg.Wait() // 所有加载完才继续
startService()
需要复用?小心 sync.Cond 的锁生命周期
如果要像 CyclicBarrier 那样“用完还能 reset”,就得手动管理条件变量。关键陷阱在于:sync.Cond 必须绑定一个已初始化的 *sync.Mutex,且该 mutex 在整个生命周期中不能被释放或替换。
常见错误是把 Cond 做成局部变量,或每次 reset 都 new 一个新的 Cond,导致旧的 Wait() 永远没人 Signal() 或 Broadcast()。
实操要点:
- 用指针字段持有
*sync.Cond,初始化一次,复用到底 -
Reset()方法里只清计数器,不重建Cond或Mutex - 所有
Wait()调用必须在mutex.Lock()之后、mutex.Unlock()之前 - 广播用
cond.Broadcast(),不是Signal(),否则可能漏唤醒
性能和兼容性:别为 Barrier 过度设计
纯 channel 实现(比如用带缓冲 channel 做计数)看似“更 Go”,但实际有额外内存分配和调度开销;而 sync.WaitGroup 是原子操作,零分配,最快。标准库的 sync 包在 1.20+ 已针对高并发场景做过优化,没必要自己 reinvent。
跨版本兼容性上,sync.WaitGroup 自 Go 1.0 就稳定,sync.Cond 行为也无变化。但如果你在用 go:build 条件编译或 CGO 环境,要注意 Cond 底层依赖系统 futex,在某些嵌入式平台可能降级为 mutex 轮询。
真正容易被忽略的是:Barrier 不解决数据竞争。即使协程“齐步走”,它们访问的共享变量仍需额外同步(如 sync.Map 或互斥锁)。齐步 ≠ 安全。










