生产级资产扫描器应采用「协程池 + 限流」组合,而非裸启成千上万个 goroutine;需自建轻量协程池控制并发数,配合 rate.limiter 在发包层限速,并用带缓冲 channel 安全收集结果,退出时显式 cancel context 防泄漏。

直接上结论:用 go 启动成千上万个扫描任务不是最优解,真正在生产级资产扫描器里起作用的是「协程池 + 限流」组合,否则要么打崩自己内存,要么被目标封 IP。
为什么裸写 go scanPort(host, port) 会出问题
新手常写的代码是遍历端口列表,每个都 go scanPort(...) —— 看似并发,实则危险。
- 默认 Goroutine 初始栈约 2KB,10 万个任务 ≈ 200MB 内存,还没发包就 OOM
- 无节制发包(比如每秒上千个 TCP SYN)会被防火墙识别为扫描行为,触发
ICMP unreachable或直接 DROP - 没有任务队列和 Worker 复用,每次新建 Goroutine 的调度开销叠加后反而拖慢整体吞吐
- 错误无法集中捕获:某个
scanPortpanic 会导致整个程序崩溃,除非每个都套recover
ants 或自建协程池怎么选
社区常用 ants 库(github.com/panjf2000/ants/v2),但它对扫描类场景有隐性缺陷;更推荐手写轻量池。
-
ants默认使用无缓冲 channel 作为任务队列,高并发下容易阻塞,且不暴露底层 Worker 生命周期控制 - 扫描任务通常带超时和重试逻辑,需要在 Worker 内部统一处理
context.WithTimeout,而ants.Submit不方便传 context - 自建池只需一个
chan *ScanTask+ 固定数量的 for-select 循环 Worker,50 行内可完成,可控性更强 - 示例关键结构:
type ScanTask struct { Host string; Port int; Ctx context.Context },Worker 从 channel 取任务,用net.DialTimeout扫描,结果写回另一 channel
限流必须作用在发包层,不是 sleep
很多人在 Worker 里加 time.Sleep(10 * time.Millisecond),这是错的 —— 它限的是 Worker 空闲时间,不是发包速率。
- 真正要限的是「单位时间内发出的 TCP connect 尝试数」,应使用
golang.org/x/time/rate.Limiter - 把
limiter := rate.NewLimiter(rate.Every(time.Second/100), 100)放在全局,每次 dial 前调用limiter.Wait(ctx) - 注意:rate.Limiter 不阻塞 goroutine,而是控制令牌发放节奏;若需严格保序(如按 IP 分组限流),得为每个目标 host 单独建 limiter 实例
- 避免和协程池限流混淆:协程池控并发数(比如最多 200 个 Worker),rate.Limiter 控发包频次(比如每秒最多 100 包),两者正交
扫描结果怎么安全收集
别用全局 map + sync.Mutex,高频写入下锁争用严重;channel 是更自然的选择。
- 开一个带缓冲的
resultCh := make(chan ScanResult, 1000),所有 Worker 发送结果到它 - 单独起一个 goroutine 消费:
for res := range resultCh { /* 存 DB / 写文件 */ } - 如果需去重或聚合(比如同一 IP 多个端口开放),消费侧再做逻辑,不要让 Worker 负责
- 切记关闭 channel:当所有 Worker 结束后,
close(resultCh),否则消费者会永远阻塞
最易被忽略的一点:扫描器退出时,没显式 cancel 所有 pending 的 context,导致后台 goroutine 泄漏 —— 这类泄漏在长时间运行的扫描任务中会缓慢吃光内存。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











