Go 入门(27):练习——并发爬虫
更新时间:2026-09-01。本文是
languages/go/beginner/入门层第 27 篇,接 HTTP 接口服务。综合练习:写一个并发爬虫,能同时爬取多个 URL,限制并发数,用 channel 收集结果。这是 goroutine + channel + WaitGroup + select 的完整实战。
需求
- 接收一串 URL 列表
- 并发爬取,限制最大并发数(比如 5 个)
- 每个 URL 爬完后,打印状态码和页面大小
- 全部爬完后,统计成功/失败数量
- 整个程序在 main 函数里等所有爬虫跑完才退出
用到的知识
goroutine并发执行channel传递任务和结果sync.WaitGroup等待完成net/http发送 HTTP 请求select配合信号量控制并发数
实现
go
package main
import (
"fmt"
"io"
"log"
"net/http"
"sync"
"time"
)
type Result struct {
URL string
StatusCode int
Size int64
Duration time.Duration
Err error
}
func main() {
urls := []string{
"https://httpbin.org/delay/1",
"https://httpbin.org/delay/2",
"https://httpbin.org/status/404",
"https://httpbin.org/status/500",
"https://httpbin.org/get",
"https://httpbin.org/anything",
"https://httpbin.org/json",
"https://httpbin.org/robots.txt",
}
// 并发爬取,结果从 channel 收集
results := crawl(urls, 3) // 最多 3 个并发
// 统计
var success, fail int
for res := range results {
if res.Err != nil {
fmt.Printf("[FAIL] %s: %v\n", res.URL, res.Err)
fail++
} else {
fmt.Printf("[OK] %s (status=%d, size=%d, %v)\n",
res.URL, res.StatusCode, res.Size, res.Duration)
success++
}
}
fmt.Printf("\n完成: 成功 %d, 失败 %d\n", success, fail)
}
// crawl 爬取 URL 列表,限制最大并发数,返回结果 channel
func crawl(urls []string, maxConcurrency int) <-chan Result {
results := make(chan Result, len(urls))
var wg sync.WaitGroup
// 用带缓冲 channel 做信号量,控制并发数
sem := make(chan struct{}, maxConcurrency)
for _, url := range urls {
wg.Add(1)
url := url // 捕获循环变量
go func() {
defer wg.Done()
sem <- struct{}{} // 获取信号量
defer func() { <-sem }() // 释放信号量
results <- fetch(url)
}()
}
// 等所有 goroutine 完成,关闭结果 channel
go func() {
wg.Wait()
close(results)
}()
return results
}
// fetch 请求单个 URL,返回结果
func fetch(url string) Result {
start := time.Now()
resp, err := http.Get(url)
if err != nil {
return Result{URL: url, Err: err, Duration: time.Since(start)}
}
defer resp.Body.Close()
size, err := io.Copy(io.Discard, resp.Body)
if err != nil {
return Result{URL: url, Err: err, Duration: time.Since(start)}
}
return Result{
URL: url,
StatusCode: resp.StatusCode,
Size: size,
Duration: time.Since(start),
}
}运行
bash
go run crawler.go
[OK] https://httpbin.org/delay/1 (status=200, size=0, 1.24s)
[OK] https://httpbin.org/get (status=200, size=308, 0.87s)
[OK] https://httpbin.org/delay/2 (status=200, size=0, 2.15s)
[OK] https://httpbin.org/json (status=200, size=418, 0.91s)
[OK] https://httpbin.org/anything (status=200, size=335, 0.93s)
[FAIL] https://httpbin.org/status/500: 500 Internal Server Error
[OK] https://httpbin.org/status/404 (status=404, size=0, 0.89s)
[OK] https://httpbin.org/robots.txt (status=200, size=129, 0.92s)
完成: 成功 7, 失败 1核心设计:信号量控制并发
用 sem := make(chan struct{}, maxConcurrency) 做信号量:
go
sem <- struct{}{} // 获取——缓冲区满时阻塞,达到并发上限
defer func() { <-sem }() // 释放——让出位置给下一个 goroutine这就是 Go 并发编程里用 channel 做限流的经典模式,比用 Mutex 管理计数器更简洁。
扩展练习
- 加超时:用
http.Client{Timeout: 5 * time.Second}限制单个请求最长 5 秒 - 支持缓存:爬过的 URL 不再重复爬,用
map记录 - 深度爬取:从页面里解析出链接,再爬下一层
- 输出报告:爬完生成一个 JSON 报告,包含所有 URL 的状态和时间
相关与延伸
本系列到此结束,你已掌握 Go 入门核心。下一篇:进阶层 goroutine 与 channel 深度解析;Go 并发模型和 C++ 线程模型的对比,见 C 并发。
一句话总结
并发爬虫:综合了 goroutine 并发执行、channel 传递任务/结果、WaitGroup 等待完成、信号量控制并发数、select 配合超时等 Go 并发核心概念。这是 Go 并发编程的"Hello World"——用 goroutine 不是复杂的事,用 channel 做同步不是复杂的事,合在一起就是 Go 的并发哲学。