go环境装完不能直接跑爬虫,需确认三件事:一是go版本不低于1.19以支持可靠超时控制;二是http.client必须配置transport并设maxidleconns等参数防文件描述符耗尽;三是并发须用带缓冲channel作信号量精确控流。

Go 环境装完就能跑爬虫?别急,先确认这三件事
Go 1.19+ 是硬门槛,低于这个版本的 http.Client 缺少关键超时控制能力,context.WithTimeout 在请求中容易失效;go version 输出必须包含 go1.19 或更高;Windows 用户注意 PATH 里不能有空格路径(比如 C:\Program Files\Go),否则 go mod 会静默失败;macOS M 系列芯片用户若用 Homebrew 安装,务必执行 brew install go 而非 brew install golang——后者是旧别名,可能拉取过期包。
http.Client 不配 Transport 就并发?等着被 too many open files 杀掉
裸调 http.Get 或直接 new http.Client{} 没设 Transport,等于每请求开新 TCP 连接、永不复用、不设空闲上限。100 个 goroutine 并发,极可能触发系统级文件描述符耗尽,错误信息就是 fork/exec /usr/bin/curl: too many open files 或更隐蔽的 DNS 解析卡死。
-
MaxIdleConns和MaxIdleConnsPerHost必须显式设为相同值(如100),否则跨域名连接池形同虚设 -
IdleConnTimeout设太短(2m)可能被服务端静默断连后还傻等,建议30 * time.Second -
TLSHandshakeTimeout必须设(如5 * time.Second),否则 TLS 握手卡住会拖垮整个 client 实例 - 别漏掉
ResponseHeaderTimeout和ExpectContinueTimeout,尤其在目标站启用了 HTTP/2 或 Expect: 100-continue 时
正确写法:
client := &http.Client{
Timeout: 10 * time.Second,
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
IdleConnTimeout: 30 * time.Second,
TLSHandshakeTimeout: 5 * time.Second,
ResponseHeaderTimeout: 8 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
},
}
用 chan struct{} 做信号量?容量和释放时机错一点就泄漏
无缓冲 channel(make(chan struct{}))无法限流,只适合同步通知;用 sync.WaitGroup 只能等结束,压根不管并发数。真正可控的并发必须靠带缓冲 channel 当信号量,但极易出错。
- 声明时容量即最大并发数:
sem := make(chan struct{}, 100),不是 1000 也不是 10 - 获取令牌必须在 goroutine 内部:写成
sem ,且必须在 defer 前——否则 panic 后没释放 - 释放必须用
,不是 <code>sem ,后者会阻塞 - 释放操作必须包裹在 defer 里,且 defer 要紧贴 goroutine 开头,避免逻辑分支绕过
典型安全写法:
go func(u string) {
sem resp, err := client.Get(u)
if err != nil {
log.Printf("fetch %s failed: %v", u, err)
return
}
defer resp.Body.Close()
// ... 处理响应
}(url)
goroutine 泄漏比性能差更致命:没关 body、没读完 resp.Body、没回收 channel
最常见的泄漏不是 goroutine 本身没退出,而是它持有的资源(尤其是 resp.Body)一直占着连接不放。一个没 defer resp.Body.Close() 的请求,会让 Transport 认为连接还在用,后续复用失败,最终耗尽连接池。
-
resp.Body必须 close,且必须在 goroutine 内部 close,不能丢给上层函数处理 - 如果用
io.Copy或io.ReadAll读 body,要检查返回 error,否则读到一半出错,body 没关干净 - channel 作为任务队列时,生产者关闭后消费者必须用
for range,不能用for { select { case x := ——后者收不到关闭信号,会永久阻塞 - Context 超时后,goroutine 必须主动退出,不能靠 runtime 强杀;检查
ctx.Err() != nil应该贯穿整个请求生命周期
最简防泄漏模板:
go func(ctx context.Context, u string) {
sem req, _ := http.NewRequestWithContext(ctx, "GET", u, nil)
resp, err := client.Do(req)
if err != nil {
return
}
defer resp.Body.Close() // 关在这里,不是外面
// 读取 body 必须完整或明确中断
_, _ = io.Copy(io.Discard, resp.Body)
}(ctx, url)
真正的高并发爬虫瓶颈不在开多少 goroutine,而在连接复用是否到位、信号量是否守恒、资源是否及时归还。这些点任何一个松动,跑一小时就内存暴涨或连接卡死——不是代码写得不够“并发”,而是没守住并发的边界。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











