在 Go 语言的并发编程中,sync.WaitGroup 无疑是最常用的同步原语之一。从 Go 1.0 开始,它就陪伴着我们处理各种 goroutine 同步场景。
然而,多年来我们一直沿用着固定的使用模式:
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
// 执行业务逻辑
}()
wg.Wait()
这段代码你是否觉得眼熟?没错,这就是我们写了无数遍的模板代码。忘记写 wg.Add(1) 或者忘记 defer wg.Done() 就是一个 BUG,即便你都没有忘记,但这样的重复代码是否让你觉得有些繁琐?
Go 1.25 敏锐地捕捉到了这个痛点,为 WaitGroup 引入了全新的 Go() 方法,让并发编程变得更加简洁和优雅。
传统 WaitGroup 写法
经典使用模式的问题
让我们先看一个典型的例子:
func processItems(items []string) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i string) {
defer wg.Done()
process(i)
}(item)
}
wg.Wait()
}
这段代码看起来没有问题,但仔细想想,它存在几个可以改进的地方:
1. 代码冗余
每次启动 goroutine 都要写 wg.Add(1)、defer wg.Done() 这样的模板代码。虽然使用 defer 可以确保正确性,但重复的代码增加了编写和阅读的成本。
2. 心智负担
即使是经验丰富的开发者,每次都要记得使用 defer 关键字,确保 Done() 在任何情况下都会被调用。这种重复的决策消耗了不必要的认知资源。
3. 可读性不佳
业务逻辑被同步代码包裹,核心的代码意图不够清晰。
Go() 方法的诞生
什么是 Go() 方法?
Go 1.25 引入的 Go() 方法,本质上是一个封装了 Add(1) 和 Done() 的辅助方法。它的签名非常简单:
func (wg *WaitGroup) Go(f func())
使用方法也极其直观:
var wg sync.WaitGroup
wg.Go(func() {
// 执行业务逻辑
// 无需手动调用 Add 和 Done
})
wg.Wait()
核心实现原理
Go() 方法的实现简洁而优雅:
func (wg *WaitGroup) Go(f func()) {
wg.Add(1)
go func() {
defer wg.Done()
f()
}()
}
看到了吗?它利用 defer 确保 Done() 一定会被调用,即使函数中发生了 panic。这个简单的设计解决了我们之前提到的所有问题。
Go() 方法解决了什么问题
1. 代码更简洁
自动处理同步原语
使用 Go() 方法后,Add(1) 和 Done() 的调用被封装在方法内部,开发者无需关心这些细节:
func processItems(items []string) {
var wg sync.WaitGroup
for _, item := range items {
wg.Go(func() {
process(item)
})
}
wg.Wait()
}
内置 defer 保证
Go() 内部使用 defer wg.Done(),确保无论函数如何退出,Done() 都会被执行,即使发生 panic。
2. 代码简洁性
减少模板代码
对比一下两种写法:
// 传统写法
wg.Add(1)
go func() {
defer wg.Done()
// 业务逻辑
}()
// Go() 方法
wg.Go(func() {
// 业务逻辑
})
代码行数减少了 40%,可读性显著提升。
3. 意图更清晰
使用 Go() 方法后,代码的意图更加明确:
// 一眼就能看出这是并发执行的任务
wg.Go(func() { fetchUser() })
wg.Go(func() { fetchOrder() })
wg.Go(func() { fetchProduct() })
写在最后
sync.WaitGroup 的 Go() 方法虽然只是一个语法糖,却体现了 Go 语言设计哲学的精髓:
- 简单优于复杂:用简洁的封装减少模板代码
- 效率优于形式:让开发者专注于业务逻辑而非同步细节
- 实用优于理论:解决实际开发中的痛点
学习就是不断进化的过程,进化让编程变得更加美好。