goroutine 扫描端口卡住主因是 channel 未关闭,导致 for range ch 永久阻塞;须在 wg.wait() 后立即 close(ch),不可 defer;并发需限流,用带缓冲的 sem := make(chan struct{}, 100) 控制。

goroutine 扫描端口时程序卡住不动?一定是 channel 没关
Go 中 for range ch 会一直等新数据,直到 ch 被 close()。常见写法里用 sync.WaitGroup 等所有 goroutine 完成,但漏掉 close(ch),结果主 goroutine 永远卡在循环里。
必须在 wg.Wait() 后立刻 close(ch),不能靠 defer(因为 defer 在函数返回时才执行,而主 goroutine 正卡在 range)。
- 错误写法:
go func() { wg.Wait(); }()—— 匿名 goroutine 里调wg.Wait(),但没 close,主 goroutine 仍卡在for range ch - 正确顺序:先
wg.Wait(),再close(ch),最后才for range ch或把它包进另一个 goroutine - 更稳妥的做法是把
close(ch)放进一个单独的 goroutine:go func() { wg.Wait(); close(ch) }()
并发数不加限制,轻则 OOM,重则被目标封 IP
直接对每个端口起一个 goroutine(比如扫 1–65535),等于瞬间发起上万个 TCP 连接请求。系统文件描述符很快耗尽,net.DialTimeout 大量返回 dial tcp: too many open files;目标设备也会触发 SYN flood 防御,返回 connection refused 或干脆丢包。
必须用信号量控制并发数,最简方式是带缓冲的 chan struct{}:
- 声明:
sem := make(chan struct{}, 100)—— 最多同时跑 100 个扫描任务 - 每个 goroutine 开始前写:
sem - 结束时释放:
(建议用 <code>defer包裹) - 别用
runtime.GOMAXPROCS控制——它管的是 OS 线程数,不是业务并发度
scanPort 函数里不设超时,整个扫描器就不可控
不用 net.DialTimeout 或自己封装带 context 的 dial,单个端口探测可能卡住几十秒(比如防火墙静默丢包、中间网络设备无响应),拖垮全部并发流程。
超时值要按场景选:
- 内网探测:
200 * time.Millisecond足够,超过即判定为关闭/过滤 - 公网探测:
1 * time.Second是较平衡的选择;低于 500ms 容易误判,高于 2s 显著拖慢整体速度 - 千万别用
net.Dial—— 它没有超时,会无限等待 DNS + TCP 握手 - 如果需支持 UDP 扫描,得换用
net.ListenUDP+WriteTo+ReadFrom,且必须配独立 timeout
DNS 查询不缓存不设限,比端口扫描还容易崩
批量扫不同域名时,net.LookupIP 默认无超时、无缓存、无并发控制。一串 lookup xxx: no such host 或 context deadline exceeded 错误,八成不是网络问题,是 resolver 卡死了。
生产级做法是自己构造 *net.Resolver:
- 启用 Go 自带解析器:
PreferGo: true - 设超时:
Timeout: 2 * time.Second - 用
sync.Map缓存结果,key 是域名,value 是[]net.IP - 对同一域名,只允许一个 goroutine 实际发起查询,其余等待——可用
sync.Once或 channel 协调
真正难的从来不是“怎么并发”,而是“怎么让并发不互相踩脚”。channel 关不关、并发数压不压、超时设不设、DNS 查不查缓存——这些细节堆起来,才是一个能落地的网络探测工具和玩具代码的分水岭。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











