Go语言天然支持并发特性,在处理大尺寸切片时,通过并行执行函数调用可以显著提升程序运行效率,但如果直接启动多个goroutine操作切片,很容易出现数据竞争、主协程提前退出等问题,导致程序运行结果不符合预期。sync包中的WaitGroup工具可以有效解决这类协程同步问题,保障切片并发操作的安全性。

切片并发操作的常见问题
如果直接遍历切片启动goroutine执行函数,可能会遇到两个问题:一是主协程不会等待子goroutine执行完成就直接退出,导致任务未完成程序就终止;二是如果多个goroutine同时修改切片内容,会触发数据竞争,导致数据错误甚至程序崩溃。下面的简单示例就存在这类问题:
package main
import (
"fmt"
"time"
)
func main() {
slice := []int{1, 2, 3, 4, 5}
for _, v := range slice {
go func(val int) {
time.Sleep(time.Millisecond * 100)
fmt.Println("处理值:", val)
}(v)
}
// 主协程直接退出,子goroutine可能还没执行完成
fmt.Println("主协程结束")
}
运行上述代码会发现,主协程打印结束后程序就退出了,切片中部分值的处理逻辑没有执行,无法得到完整的处理结果。
WaitGroup核心方法说明
WaitGroup是sync包提供的结构体,用于等待一组goroutine完成执行,核心方法有三个:
Add(int):设置需要等待的goroutine数量,通常在启动goroutine前调用,参数为正整数Done():每个goroutine执行完成后调用,会将等待的计数器减1,等价于Add(-1)Wait():阻塞当前协程,直到等待的计数器归零,即所有goroutine都执行完成
用WaitGroup实现安全的切片并行调用
结合WaitGroup的三个方法,可以正确实现切片的并行函数调用,避免主协程提前退出和数据竞争问题。下面的示例演示了安全的实现方式:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
slice := []int{1, 2, 3, 4, 5}
// 声明WaitGroup实例
var wg sync.WaitGroup
// 设置需要等待的goroutine数量,等于切片长度
wg.Add(len(slice))
for _, v := range slice {
val := v
// 启动goroutine执行处理逻辑
go func() {
defer wg.Done()
time.Sleep(time.Millisecond * 100)
// 这里可以替换为实际的函数调用逻辑
result := val * 2
fmt.Printf("处理值 %d,结果: %dn", val, result)
}()
}
// 阻塞主协程,等待所有goroutine完成
wg.Wait()
fmt.Println("所有切片元素处理完成")
}
上述代码中,首先通过wg.Add(len(slice))设置需要等待的goroutine数量,每个goroutine执行时通过defer wg.Done()确保执行完成后计数器减1,主协程调用wg.Wait()阻塞直到所有任务完成,最终可以正确输出所有切片元素的处理结果。
使用注意事项
在使用WaitGroup处理切片并发时,需要注意以下几点:
- 不要在goroutine内部调用
Add方法,否则可能出现计数器还没设置完成,部分goroutine已经调用Done导致计数器提前归零的问题 - 遍历切片时,需要将当前元素赋值给局部变量再传入goroutine,避免goroutine捕获到循环变量的地址,导致所有goroutine处理的是同一个变量的最终值
- 如果并行处理过程中需要修改原切片,建议配合互斥锁
sync.Mutex使用,避免数据竞争问题 - WaitGroup实例不需要显式初始化,零值就可以直接使用,也不需要手动释放资源
带互斥锁的切片修改示例
如果并行处理需要修改切片内容,需要结合互斥锁保证操作安全,示例如下:
package main
import (
"fmt"
"sync"
"time"
)
func main() {
slice := []int{1, 2, 3, 4, 5}
var wg sync.WaitGroup
var mu sync.Mutex
wg.Add(len(slice))
for i, v := range slice {
idx := i
val := v
go func() {
defer wg.Done()
time.Sleep(time.Millisecond * 100)
// 加锁保证切片修改安全
mu.Lock()
slice[idx] = val * 3
mu.Unlock()
fmt.Printf("修改索引 %d 的值为 %dn", idx, slice[idx])
}()
}
wg.Wait()
fmt.Println("最终切片内容:", slice)
}
这个示例中,通过互斥锁对切片的修改操作加锁,确保同一时间只有一个goroutine可以修改切片内容,避免了数据竞争问题,最终可以得到正确的修改结果。