
本文详解 sync.WaitGroup 未正常完成的典型原因——未关闭用于接收结果的 channel,导致 for range 永久阻塞;并提供完整、线程安全的并发请求处理范式。
本文详解 `sync.waitgroup` 未正常完成的典型原因——未关闭用于接收结果的 channel,导致 `for range` 永久阻塞;并提供完整、线程安全的并发请求处理范式。
在 Go 并发编程中,sync.WaitGroup 是协调 goroutine 生命周期的核心工具,但其行为高度依赖开发者对同步逻辑的精确控制。你提供的代码中,wg.Wait() 虽能正确等待所有 goroutine 完成,但后续的 for link := range result { ... } 却永远无法退出——根本原因在于:未关闭 result channel,致使 range 操作持续等待新值,形成永久阻塞。
Go 规范明确规定:for range 在 channel 上会一直阻塞,直到该 channel 被显式关闭(close(result))。而你的原始代码中,result 是一个无缓冲 channel,10 个 goroutine 向其发送数据后各自调用 wg.Add(-1)(⚠️注意:此处用法错误,应为 wg.Done()),但主 goroutine 在 wg.Wait() 返回后并未关闭 channel,导致 range 无限挂起。
此外,代码中还存在两个关键隐患:
-
wg.Add(-1)的误用:Add()接收负数虽合法,但语义混乱且易出错;应统一使用wg.Done()(等价于wg.Add(-1))并在 defer 中调用,确保即使发生 panic 也能正确计数。 - 未处理并发写入竞争:多个 goroutine 同时向同一 channel 发送数据是安全的,但若后续需聚合结果(如存入 slice),则需额外同步机制。
✅ 正确做法如下(已修复全部问题):
func main() {
parameterStr := fmt.Sprintf("%vdata_id%vequ_id%v%v", SIGNKEY, DATA_ID, EQU_ID, SIGNKEY)
sign := getMD5Hash(parameterStr)
payload := map[string]string{
"equ_id": EQU_ID,
"data_id": DATA_ID,
"sign": sign,
}
payloadJson, err := json.Marshal(payload)
if err != nil {
log.Fatalln("convert payload failed:", err)
}
runtime.GOMAXPROCS(runtime.NumCPU())
var wg sync.WaitGroup
result := make(chan string, 10) // 建议设缓冲,避免 sender 阻塞
// 启动消费者 goroutine,负责读取并打印结果
go func() {
for link := range result {
fmt.Println(link)
}
}()
// 启动 10 个请求 goroutine
for i := 0; i < 10; i++ {
wg.Add(1)
go func() {
defer wg.Done() // ✅ 正确方式:defer 确保执行
getUrl(payloadJson, &wg, result)
}()
}
wg.Wait()
close(result) // ✅ 关键:关闭 channel,通知 range 结束
log.Println("Done!")
}? 补充说明:
getUrl函数内部仍建议将wg.Add(-1)替换为defer wg.Done(),并移除原wg.Add(-1)调用(当前逻辑中该行实际未生效,因wg.Wait()前wg.Add(1)已覆盖);- 使用带缓冲的 channel(如
make(chan string, 10))可提升性能,避免 sender 因 receiver 暂未读取而阻塞;- 若需收集全部结果而非仅打印,可改用
sync.Mutex+[]string,或通过close()后配合for i := 0; i 显式接收固定数量。
总结:sync.WaitGroup 本身不会“卡住”,真正导致程序停滞的是未关闭 channel 引发的 range 阻塞。牢记 “谁创建 channel,谁负责关闭” 的原则,并始终配对使用 go func() { ... }() 消费 + close() 通知,才能写出健壮、可终止的并发 Go 程序。

















