package linkcheck import ( "context" "net/http" "sync" ) type LinkStatus struct { URL string `json:"url"` StatusCode int `json:"statusCode"` OK bool `json:"ok"` Err string `json:"error,omitempty"` } type Results struct { Statuses []LinkStatus `json:"statuses"` } func Check(ctx context.Context, urls map[string]struct{}, concurrency int, client *http.Client, userAgent string, showProgress bool, progressCallback func(bool)) Results { var mu sync.Mutex var statuses []LinkStatus type job struct{ u string } jobs := make(chan job, concurrency*2) wg := sync.WaitGroup{} worker := func() { defer wg.Done() for j := range jobs { status, err := headOrGet(ctx, client, userAgent, j.u) ls := LinkStatus{URL: j.u, StatusCode: status} if err != nil { ls.Err = err.Error() } ls.OK = err == nil && status < 400 && status >= 200 mu.Lock() statuses = append(statuses, ls) mu.Unlock() if progressCallback != nil { progressCallback(ls.OK) } } } for i := 0; i < concurrency; i++ { wg.Add(1) go worker() } for u := range urls { jobs <- job{u: u} } close(jobs) wg.Wait() return Results{Statuses: statuses} } func headOrGet(ctx context.Context, client *http.Client, userAgent string, u string) (int, error) { req, _ := http.NewRequestWithContext(ctx, http.MethodHead, u, nil) req.Header.Set("User-Agent", userAgent) resp, err := client.Do(req) if err == nil { defer resp.Body.Close() if resp.StatusCode >= 200 && resp.StatusCode < 400 { return resp.StatusCode, nil } // Some servers don't support HEAD properly; fall back to GET for 4xx/405 if resp.StatusCode != http.StatusMethodNotAllowed && resp.StatusCode < 500 { return resp.StatusCode, nil } } // Fallback GET req2, _ := http.NewRequestWithContext(ctx, http.MethodGet, u, nil) req2.Header.Set("User-Agent", userAgent) resp2, err2 := client.Do(req2) if err2 != nil { return 0, err2 } defer resp2.Body.Close() return resp2.StatusCode, nil }