在Go语言开发中,并发模型的核心是Goroutine。当你在main函数中启动了一批子协程去处理任务,如果没有任何同步措施,主函数一旦执行完毕,整个进程就会立刻退出,那些还没来得及运行的子协程会被直接丢弃。这种悄无声息的数据丢失和逻辑中断,往往让初学者困惑不已。Go的运行时调度器只保证主协程结束前程序存活,对其它协程没有等待义务,因此我们必须主动建立同步机制来确认所有子协程完成。

使用sync.WaitGroup进行计数同步
sync.WaitGroup是Go标准库中最直观的协程等待工具。它的内部维护一个计数器,通过三个方法配合工作:Add用来增加等待的协程数量,Done在协程结束时减去一,Wait则会阻塞调用者直到计数器归零。这种机制特别适合任务数量在启动前就确定的场景,例如并发请求多个接口或者并行计算分片数据。
下面是一段典型的使用示例。我们在循环里启动五个协程,每个协程处理完睡眠模拟的任务后调用Done。主函数通过Wait阻塞,直到所有子协程都报告结束,这样就不会出现主程序提前退出的问题。
package main
import (
"fmt"
"sync"
"time"
)
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
time.Sleep(time.Millisecond * 200)
fmt.Println("协程", id, "完成")
}(i)
}
wg.Wait()
fmt.Println("所有子协程已完成,主程序退出")
}
使用WaitGroup时有几个细节需要注意。首先,Add的调用应该在启动协程之前进行,如果放在协程内部可能因为调度顺序导致Wait提前归零返回。其次,推荐使用defer wg.Done()来保证即使协程发生panic也能正确计数减一,避免主程序永久阻塞。最后,WaitGroup不能复制,传递给函数时应当使用指针,否则副本的计数变化不会影响原始对象。
通过channel传递完成信号实现同步
除了WaitGroup,channel也是Go并发控制的重要基石。我们可以创建一个无缓冲或者带缓冲的channel,让每个子协程在退出前向channel发送一个空结构体作为完成信号,主协程则通过接收这些信号来判断是否全部结束。这种方式在任务动态生成或者需要传递错误信息的场景中更加灵活。
下面的代码展示了如何用channel来等待十个协程。我们声明一个长度为任务数的缓冲channel,每个协程结束写入一个值,主协程通过循环接收相同次数来实现等待。相比WaitGroup,这种写法把同步状态和业务数据通道合二为一,但在纯等待场景下代码略显冗长。
package main
import (
"fmt"
"time"
)
func main() {
done := make(chan struct{}, 10)
for i := 0; i < 10; i++ {
go func(id int) {
time.Sleep(time.Millisecond * 100)
fmt.Println("任务", id, "结束")
done <- struct{}{}
}(i)
}
for i := 0; i < 10; i++ {
<-done
}
fmt.Println("通过channel确认全部任务完成")
}
channel方案的优势在于可以顺带传递错误信息。比如将done定义为chan error,协程在出错时发送具体错误,主协程收集后统一处理。但它也更容易引入死锁,例如缓冲大小设置不当或者接收次数少于发送次数,都会导致部分协程阻塞。在复杂拓扑中,我们还可以结合close(channel)与range来广播结束事件,不过这需要更严谨的设计。
两种机制的选择与常见误区
在实际项目中,WaitGroup和channel并非互斥。简单批处理用WaitGroup代码更清晰,而需要结果汇聚或动态任务流时用channel更自然。有些开发者试图用time.Sleep强行等待,这是极不可靠的做法,因为协程耗时受调度和负载影响,sleep要么浪费时间要么依然漏掉任务。还有人误用全局变量计数,在并发下产生竞态,必须配合互斥锁反而复杂。
另一个常见误区是在协程中调用Wait。Wait的设计意图是让一个协程(通常是主协程)等待一组工作协程,如果在子协程中互相等待且计数管理不当,极易形成循环等待。此外,当协程内发生panic且未用defer恢复时,WaitGroup的Done可能不被调用,主协程卡死,因此健壮的程序会在协程入口用defer做recover并确认Done执行。
理解Go调度器对Goroutine的生命周期管理,是写好并发程序的前提。无论选择哪种同步机制,核心原则都是:明确任务边界、保证计数或信号与实际协程一一对应、在异常路径上也能正确释放等待条件。只有这样,才能确保子协程全部完成后再让主程序从容退出。
Goroutinesync.WaitGroupchannel修改时间:2026-08-17 23:52:28