在 Go 中让并行 HTTP 请求更稳定:构建 Markdown Linter 的经验教训

发布: (2026年1月15日 GMT+8 21:28)
3 min read
原文: Dev.to

Source: Dev.to

在构建 gomarklint——一个基于 Go 的 Markdown 检查工具时,我遇到了一个挑战:要检查超过 100,000 行文档中的失效链接。使用 goroutine 并行化看起来是个“显而易见”的做法,但它立刻在 CI 环境中导致了不稳定的测试。Go 的速度很容易实现,稳定性才是真正的难点。下面是我实现的三种模式,兼顾了速度和稳定性。

URL 缓存

在大型文档集中,同一个 URL 会出现数十次。一个天真的并发实现会对每一次出现都发送请求,这看起来像是对目标主机的 DoS 攻击。使用 sync.Map,我实现了一个简单的 URL 缓存,确保每个唯一的 URL 只检查一次。

var urlCache sync.Map

// Check if we've seen this URL before
if val, ok := urlCache.Load(url); ok {
    return val.(*checkResult), nil
}

限制并发度

即使有缓存,同时检查大量唯一 URL 仍可能耗尽本地文件描述符或触发速率限制。使用信号量通道可以限制并发检查的数量。

maxConcurrency := 10
sem := make(chan struct{}, maxConcurrency)

for _, url := range urls {
    sem = 500 {
    time.Sleep(retryDelay * time.Duration(attempt))
    // retry...
}

缓存完整结果

最难定位的 bug 是只缓存了状态码。如果请求超时,我会存储 status: 0。后续检查取到 0 时却不知道发生了错误,导致逻辑不一致。解决办法是缓存整个结果,包括错误信息。

type checkResult struct {
    status int
    err    error
}

// Store the pointer to this struct in your cache

处理缓存击穿

即使采取了上述措施,“缓存击穿”(多个 goroutine 在同一毫秒内同时访问同一个未缓存的 URL)仍然是个潜在问题。我目前正在探索使用 golang.org/x/sync/singleflight 来解决这个问题。


如果你有为大规模并行检查调优 http.Client 的经验,欢迎在评论区或 GitHub 上分享你的想法!

Back to Blog

相关文章

阅读更多 »