用 goroutine 写并发扫描器必须限流,否则会内存爆炸、触发目标防护或资源争抢;应使用带缓冲 channel 作信号量控制并发数,并配合 sync.WaitGroup 确保任务完成。

直接说结论:用 goroutine 写并发扫描器不难,但放任不管一定会崩——不是程序挂,就是被封 IP、被限速、被目标设备拉黑。
为什么裸写 go scanPort(...) 会出问题
很多人一上来就对每个端口起一个 goroutine,比如遍历 1..65535 然后 go scanOnePort(ip, port)。看似简洁,实际踩了三类坑:
- 内存爆炸:65535 个
goroutine,哪怕每个只占 2KB 栈,也要 128MB+,还没算 socket、DNS 缓存等开销 - 连接风暴:瞬间发几千个 TCP SYN 包,触发目标防火墙的速率告警或连接拒绝(
connection refused或超时陡增) - 资源争抢:所有
goroutine共享同一套 DNS 解析器、HTTP 客户端、甚至日志写入器,没加锁或 channel 协调就会 panic 或丢数据
sync.WaitGroup + chan 控制并发数的最小可行方案
这不是“最佳实践”,而是你跑通第一个能用的版本必须写的底线代码。核心是两件事:限制同时跑的任务数、等所有任务结束再退出。
- 用带缓冲的
chan struct{}当信号量,容量即最大并发数(比如 100) - 每个任务开始前
sem ,结束后 <code><-sem - 用
sync.WaitGroup记录已启动的任务数,主 goroutine 调用wg.Wait()阻塞等待
示例片段:
立即学习“go语言免费学习笔记(深入)”;
sem := make(chan struct{}, 100)
var wg sync.WaitGroup
for _, port := range ports {
wg.Add(1)
go func(p int) {
defer wg.Done()
sem <- struct{}{} // 等待空槽位
defer func() { <-sem }() // 释放槽位
scanOnePort(target, p)
}(port)
}
wg.Wait()别硬扛 DNS 查询,用 net.Resolver 配缓存和超时
扫描器里最慢又最容易翻车的不是 TCP 连接,是 DNS 解析。默认 net.LookupIP 没超时、没缓存、没并发控制,批量查子域名时会卡死或返回 context deadline exceeded。
- 自己 new 一个
net.Resolver,设置PreferGo: true和Timeout(比如 2s) - 用
sync.Map缓存解析结果,键为域名,值为[]net.IP,避免重复查 - 对同一个域名,只允许一个 goroutine 去查,其他等待——用
sync.Once或 channel 实现
否则你会看到大量 lookup xxx: no such host 或长时间阻塞,根本不是网络问题,是 resolver 卡住了。
扫描结果怎么安全写入?别用全局 map 或 slice
多个 goroutine 往同一个 map 插数据,或者往 slice append,不用锁或 channel 就是随机 panic。常见错误写法:results = append(results, r)。
- 推荐方式:每个 worker 把结果发到一个
chan Result,由单独一个 goroutine 收集并写文件或存 DB - 如果非要本地聚合,用
sync.Map存map[string]Result,key 是"ip:port" - 写文件时别每个结果都
os.OpenFile—— 用bufio.Writer批量 flush,否则 I/O 成瓶颈
真正容易被忽略的,是结果通道没做缓冲或没关闭。漏掉 close(resultChan),收集 goroutine 会永远阻塞在 range resultChan;缓冲太小(比如 make(chan Result, 1)),worker 会卡在发送上,整个并发就僵住了。


















