
本文讲解如何修复 go 中因 goroutine 泄漏、变量捕获错误和无限启动导致的并发逻辑失效问题,并通过工作池(worker pool)模式高效、安全地并行搜索能被 1–20 整除的最小正整数。
本文讲解如何修复 go 中因 goroutine 泄漏、变量捕获错误和无限启动导致的并发逻辑失效问题,并通过工作池(worker pool)模式高效、安全地并行搜索能被 1–20 整除的最小正整数。
在初学 Go 并发时,一个常见误区是“只要加 go 就更快”——但本例恰恰反证了这一点:原始代码中,for {} 循环无休止地启动新 Goroutine,既未限制并发数量,又未正确传递循环变量 num,更缺少退出机制,最终导致程序卡死、内存耗尽或永远无输出。
? 核心问题剖析
-
变量捕获错误:
go func(x int) { ... }(num)看似传值,但若写成go func() { ... }()(未显式传参),则闭包会捕获循环变量num的地址,所有 Goroutine 共享同一变量,造成竞态与逻辑错乱; -
无限 Goroutine 启动:
for {}无终止条件,每轮都wg.Add(1)却只在满足条件时wg.Done(),wg.Wait()永远无法返回; - 缺乏结果优先级与提前退出:即使某个 Goroutine 找到答案,其他仍在运行,且无通道同步通知主协程终止;
- 资源失控:未限制并发数,可能创建数百万 Goroutine,远超系统承载能力。
✅ 正确解法:基于 Worker Pool 的结构化并发
我们采用经典的 生产者-消费者 + 工作池 模式:
- 使用固定数量的 Worker(通常为
runtime.NumCPU())消费任务; - 生产者按批次(如每
step = 100000个数为一单位)分发待检区间; - 任一 Worker 发现满足条件的数,立即通过
result通道通知主协程,主协程打印结果后return,自然终止所有 Goroutine(因jobs通道未关闭,Worker 在range jobs时阻塞,但程序已退出)。
以下是优化后的完整可运行代码:
package main
import (
"fmt"
"runtime"
)
func divide(num int) bool {
for i := 1; i <= 20; i++ {
if num%i != 0 {
return false
}
}
return true
}
const step = 100000 // 每次分配一个数值区间,避免粒度过细
func main() {
result := make(chan int, 1) // 缓冲通道,避免 Worker 阻塞
jobs := make(chan int, 100) // 缓冲任务通道,提升吞吐
// 启动 Worker 池
for w := 0; w < runtime.NumCPU(); w++ {
go func() {
for n := range jobs {
// 检查 [n, n+step) 区间内所有数
for i := n; i < n+step; i++ {
if divide(i) {
result <- i
return // 找到即退出当前 Worker,减少冗余计算
}
}
}
}()
}
// 生产者:逐批提交起始数字
num := 1
for {
select {
case jobs <- num:
num += step
case x := <-result:
fmt.Println("Smallest number is:", x)
return // 主协程退出,程序结束
}
}
}⚠️ 关键注意事项
-
通道缓冲很重要:
result设为buffered(1)可确保首个结果不阻塞 Worker;jobs缓冲避免生产者因 Worker 处理慢而卡住; -
Worker 内提前
return:一旦找到解,该 Worker 不再处理后续数字,降低无效计算; -
无需显式
sync.WaitGroup:依赖通道通信与主协程return自然终结,更符合 Go 的 CSP 哲学; -
步长
step需权衡:太小 → 任务调度开销大;太大 → 首个结果延迟高。实践中可基于1–20的 LCM(232792560)预估范围动态调整; - 此方案仍非最优数学解法:真正高效方式是质因数分解求 LCM,但本例聚焦并发工程实践。
通过以上重构,程序不仅稳定输出 232792560,还具备良好的可扩展性与资源可控性——这才是 Go 并发编程的正确打开方式。

















