Go语言不保证goroutine执行顺序,需用同步机制协调时序;sync.WaitGroup适用于等待多个任务全部完成,Add()须在启动前调用,Done()推荐defer调用。

Go 语言本身不保证 goroutine 的执行顺序,go 启动的多个协程默认是并发、无序调度的。若需“控制访问顺序”,本质不是让 goroutine 按代码顺序执行,而是通过同步机制协调它们对共享资源的操作时序——比如确保 A 完成后再执行 B,或限制同一时刻最多 N 个 goroutine 访问某服务。
用 sync.WaitGroup 等待一组 goroutine 按逻辑顺序完成
当你需要“先启动多个任务,最后统一等待全部结束”,sync.WaitGroup 是最直接的选择。它不控制执行顺序,但能明确表达“这批操作必须都做完,我再往下走”的依赖关系。
-
WaitGroup.Add()必须在 goroutine 启动前调用(通常在go语句之前),否则可能因竞态导致计数错误 - 每个 goroutine 结束前必须调用
Done(),推荐用defer wg.Done()避免遗漏 - 不要在循环中反复
Wait(),它会阻塞直到计数归零;多次调用Wait()是安全的,但无实际意义
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("task %d done\n", id)
}(i)
}
wg.Wait() // 主协程在此阻塞,直到全部 task 打印完成
用 channel 实现严格的串行化或流水线顺序
若需强制“一个接一个执行”(例如模拟单线程处理队列),最可靠的方式是用带缓冲或无缓冲 channel 作为任务分发/结果收集的管道。channel 的发送与接收天然具有同步和顺序语义。
- 无缓冲 channel(
make(chan int)):每次send都要等对应recv准备好,天然形成“请求-响应”顺序链 - 用
range遍历 channel 可按发送顺序逐个接收,适合构建 worker pipeline - 避免在多个 goroutine 中同时向同一无缓冲 channel 发送——会永久阻塞,除非有确定的接收方
ch := make(chan int, 1) // 缓冲为 1,允许一次未被消费的发送
go func() { ch <- 1 }()
go func() { ch <- 2 }()
// 两次发送不会阻塞,但接收仍按 1、2 顺序
fmt.Println(<-ch) // 1
fmt.Println(<-ch) // 2
用 sync.Mutex 或 sync.RWMutex 控制临界区访问顺序(非执行顺序)
锁解决的是“谁先拿到资源”的竞争问题,而不是“谁先运行”。多个 goroutine 同时尝试 Lock(),谁抢到谁进,操作系统调度决定,不可预测。但它能确保:一旦某个 goroutine 进入临界区,其他必须排队等待。
立即学习“go语言免费学习笔记(深入)”;
-
Mutex适用于读写都频繁且写占比不低的场景;RWMutex在读多写少时更高效,允许多个 reader 并发,但 writer 会独占 - 务必检查是否已解锁——忘记
Unlock()会导致死锁;建议用defer mu.Unlock() - 锁的粒度越小越好,避免把无关逻辑包进
Lock()/Unlock()之间
var mu sync.Mutex
var counter int
go func() {
mu.Lock()
defer mu.Unlock()
counter++
}()
别误用 runtime.Gosched() 或 time.Sleep() “模拟顺序”
这类做法看似能让 goroutine “错开执行”,实则脆弱且不可靠:
-
Gosched()只是让出当前 P,不保证下一个谁运行,也不保证调度时机 -
time.Sleep()依赖时间精度,受系统负载影响大,在 CI 或高负载环境极易失败 - 它们掩盖了真正的问题:缺乏明确的同步契约。应该用 channel、WaitGroup、锁等显式通信机制替代“碰运气”式的延时
真正难的不是让 goroutine 按某种顺序跑起来,而是清晰定义“顺序”到底指什么——是启动顺序?完成顺序?还是对某个变量的修改顺序?不同的语义对应完全不同的工具和模式。混淆这些,就容易写出看似工作、实则隐藏竞态的代码。










