
本文详解 Go 端口扫描中因未关闭通道导致 goroutine 死锁的问题,提供符合并发最佳实践的修复方案,并强调 close()、sync.WaitGroup 与通道协作的关键逻辑。
本文详解 go 端口扫描中因未关闭通道导致 goroutine 死锁的问题,提供符合并发最佳实践的修复方案,并强调 `close()`、`sync.waitgroup` 与通道协作的关键逻辑。
在 Go 中实现高效的端口扫描,核心在于合理利用 goroutine 实现并发,同时确保资源清理与同步语义正确。你当前的代码逻辑整体结构合理:为每个端口启动独立 goroutine、通过带缓冲通道收集结果、用 sync.WaitGroup 等待全部完成——但缺失了一个关键收尾动作:必须在所有 goroutine 发送完毕后显式关闭通道。
否则,for elem := range ch 将永远阻塞,因为 range 在通道未关闭时会持续等待新值(即使缓冲区已空),而你的主 goroutine 又卡在此处,无法继续执行,从而造成死锁。
✅ 正确做法是在 wg.Wait() 之后、读取通道前调用 close(ch):
// ... 启动所有 goroutine 后
wg.Wait()
close(ch) // ← 关键修复:通知接收方“数据已发送完毕”
for elem := range ch {
results = append(results, elem)
}
⚠️ 注意事项:
- close(ch) 只能由发送方调用,且仅需调用一次;多次关闭会 panic。
- 通道关闭后,仍可安全读取已缓存的数据,range 会在读完所有值后自动退出。
- 缓冲通道容量设为 len(splitPorts) 是合理的,避免 goroutine 因通道满而阻塞(但不关闭通道,缓冲区再大也无济于事)。
- wg.Add(1) 必须在 goroutine 启动前调用(你已正确放在 go func() 外部),否则存在竞态风险。
? 进阶建议(提升健壮性):
- 使用 context.Context 支持超时或取消,避免单个连接拖慢整体扫描;
- 对高并发场景(如扫描上千端口),建议限制 goroutine 数量(例如用 worker pool),防止系统资源耗尽;
- 错误处理可细化:net.OpError 可区分是连接拒绝(IsTimeout()/IsTemporary())还是目标不可达,便于结果分类。
总结:Go 的并发模型强大而简洁,但“启动 goroutine → 发送 → 等待 → 关闭通道 → 接收”这一闭环缺一不可。close() 不是可选项,而是通道驱动型并发流程的必要终结符。掌握它,是写出可靠、可维护并发代码的第一课。











