必须用 sync.WaitGroup 显式跟踪并发请求生命周期,wg.Add(1)需在goroutine启动前调用,wg.Done()建议defer调用;每个请求需独立context.WithTimeout防止单点拖垮整体;结果应通过带缓冲channel(容量=len(urls))安全汇总。

用 sync.WaitGroup 控制并发请求生命周期
直接起一堆 goroutine 而不等待完成,会导致主函数提前退出、结果丢失。必须用 sync.WaitGroup 显式跟踪所有请求是否结束。
常见错误是忘记调用 wg.Add(1) 或在 goroutine 外调用 wg.Done(),导致死锁或 panic。
-
wg.Add(1)必须在启动 goroutine 前调用(不能放在 goroutine 内部) -
wg.Done()必须在每个 goroutine 结束前调用,建议用defer wg.Done() - 主 goroutine 调用
wg.Wait()会阻塞,直到所有子 goroutine 完成
用 context.WithTimeout 防止单个请求拖垮整体
并发请求中某个 URL 响应慢或挂死,会卡住整个 wg.Wait(),必须为每个请求单独设置超时。
不能只给 http.Client 设置全局 Timeout,那会影响所有请求;也不能复用同一个 context.Context,否则一个超时会 cancel 所有请求。
立即学习“go语言免费学习笔记(深入)”;
- 对每个请求调用
context.WithTimeout(ctx, 5*time.Second)创建独立子 context - 把子 context 传给
http.NewRequestWithContext(),而非http.Get() - 记得调用
cancel()(用defer cancel()最安全)
用带缓冲的 channel 汇总结果并避免竞态
多个 goroutine 同时写入一个 slice 会引发 data race,用 channel 中转最自然。但无缓冲 channel 可能阻塞 goroutine,影响并发吞吐。
缓冲大小设为请求数量可避免阻塞,也防止结果丢失(即使主 goroutine 还没开始读)。
- 声明
results := make(chan *Result, len(urls)) - 每个 goroutine 写入:
results - 主 goroutine 在
wg.Wait()后循环接收:for i := 0; i
完整示例:并发请求 3 个 URL 并汇总状态码
package mainimport ( "context" "fmt" "net/http" "sync" "time" )
type Result struct { URL string StatusCode int Err error }
func main() { urls := []string{ "https://www.php.cn/link/5f69e19efaba426d62faeab93c308f5c", "https://www.php.cn/link/98a733901e53052474f2320d0a3a9473", "https://www.php.cn/link/874b2add857bd9bcc60635a51eb2b697", }
results := make(chan *Result, len(urls)) var wg sync.WaitGroup ctx := context.Background() for _, url := range urls { wg.Add(1) go func(u string) { defer wg.Done() reqCtx, cancel := context.WithTimeout(ctx, 3*time.Second) defer cancel() req, err := http.NewRequestWithContext(reqCtx, "GET", u, nil) if err != nil { results <- &Result{URL: u, Err: err} return } resp, err := http.DefaultClient.Do(req) if err != nil { results <- &Result{URL: u, Err: err} return } defer resp.Body.Close() results <- &Result{URL: u, StatusCode: resp.StatusCode} }(url) } wg.Wait() close(results) for r := range results { if r.Err != nil { fmt.Printf("ERROR %s: %v\n", r.URL, r.Err) } else { fmt.Printf("OK %s: %d\n", r.URL, r.StatusCode) } }}
注意
http.DefaultClient本身是并发安全的,但它的Transport默认连接池有限(MaxIdleConnsPerHost=100),高并发时不用额外配置;真正容易被忽略的是:每个resp.Body必须关闭,否则连接不会释放,后续请求可能卡住。










