
本文详解如何将顺序遍历切片并调用函数的操作改为真正的并发执行,避免常见闭包捕获错误和切片越界问题,使用 sync.waitgroup 安全收集结果。
在 Go 中实现切片元素的并行处理,核心目标是:对每个输入值独立启动 goroutine 执行计算(如 double(i)),不相互阻塞,最终按原始顺序聚合结果。初学者常误用 channel 切片(如 []chan int)并陷入索引越界或闭包变量捕获陷阱——这正是原代码中 chans[i] = make(chan int) panic 的根源:chans 被声明为零长度切片,却直接通过索引赋值。
更简洁、安全且符合 Go 习惯的做法是:预分配结果切片 + sync.WaitGroup 协调完成信号 + 显式传参避免闭包陷阱。以下是优化后的完整实现:
package main
import (
"fmt"
"sync"
"time"
)
func double(i int) int {
result := 2 * i
fmt.Printf("double(%d) = %d\n", i, result)
time.Sleep(500 * time.Millisecond) // 模拟耗时操作(原代码为 500ms,非 500ns)
return result
}
func notParallel(arr []int) []int {
outArr := make([]int, 0, len(arr))
for _, i := range arr {
outArr = append(outArr, double(i))
}
return outArr
}
func parallel(arr []int) []int {
n := len(arr)
outArr := make([]int, n) // 预分配,确保索引安全
var wg sync.WaitGroup
for i, num := range arr {
wg.Add(1)
// 关键:显式将 i 和 num 作为参数传入 goroutine
// 避免循环变量被所有 goroutine 共享(闭包陷阱)
go func(idx int, value int) {
defer wg.Done()
outArr[idx] = double(value)
}(i, num)
}
wg.Wait() // 主协程等待所有 goroutine 完成
return outArr
}
func main() {
arr := []int{7, 8, 9}
fmt.Println("=== 顺序执行 ===")
start := time.Now()
seq := notParallel(arr)
fmt.Printf("结果: %v, 耗时: %v\n", seq, time.Since(start))
fmt.Println("\n=== 并发执行 ===")
start = time.Now()
conc := parallel(arr)
fmt.Printf("结果: %v, 耗时: %v\n", conc, time.Since(start))
}关键要点说明:
- ✅ 预分配结果切片:outArr := make([]int, len(arr)) 确保后续 outArr[i] = ... 不会 panic;
- ✅ WaitGroup 精确计数:wg.Add(1) 在 goroutine 启动前调用,defer wg.Done() 保证异常时也能释放;
- ✅ 闭包安全传参:go func(idx, value int) { ... }(i, num) 将当前循环变量值拷贝传入,杜绝 i 和 num 在循环结束后被覆盖导致的竞态;
- ⚠️ 无需 channel 切片:本场景只需“写入固定位置”,channel 带来额外复杂度(缓冲管理、接收阻塞、关闭逻辑),反而降低可读性与性能;
- ? 时间验证:3 个元素各耗时 ~500ms,顺序执行约 1500ms,而并发执行接近 500ms(取决于调度开销),效果显著。
若未来需处理不确定数量结果、或需流式消费中间结果,则 channel 方案才具优势——但此时应使用单个 chan T 配合 for range 接收,而非 channel 切片。对于确定长度、顺序敏感的并行映射(map-reduce 中的 map 阶段),WaitGroup + 预分配切片 是最推荐的 Go 模式。









