Frontend Labs
⋯

25. Terminal project: a concurrent URL health-checker (worker pool)

Goal: Week 2's mini-project, built out in full — a worker pool with a bounded concurrency limit and an overall deadline.

package main

import (
	"bufio"
	"context"
	"fmt"
	"net/http"
	"os"
	"sync"
	"time"
)

type Result struct {
	URL      string
	Status   int
	Latency  time.Duration
	Err      error
}

func checkOne(ctx context.Context, client *http.Client, url string) Result {
	start := time.Now()
	req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
	if err != nil {
		return Result{URL: url, Err: err}
	}
	resp, err := client.Do(req)
	if err != nil {
		return Result{URL: url, Err: err, Latency: time.Since(start)}
	}
	defer resp.Body.Close()
	return Result{URL: url, Status: resp.StatusCode, Latency: time.Since(start)}
}

func worker(ctx context.Context, client *http.Client, urls <-chan string, results chan<- Result, wg *sync.WaitGroup) {
	defer wg.Done()
	for url := range urls {
		select {
		case <-ctx.Done():
			return
		default:
			results <- checkOne(ctx, client, url)
		}
	}
}

func main() {
	concurrency := 10
	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	urls := make(chan string)
	results := make(chan Result)
	client := &http.Client{Timeout: 5 * time.Second}

	var wg sync.WaitGroup
	for i := 0; i < concurrency; i++ {
		wg.Add(1)
		go worker(ctx, client, urls, results, &wg)
	}

	go func() {
		defer close(urls)
		scanner := bufio.NewScanner(os.Stdin)
		for scanner.Scan() {
			select {
			case urls <- scanner.Text():
			case <-ctx.Done():
				return
			}
		}
	}()

	go func() {
		wg.Wait()
		close(results)
	}()

	for r := range results {
		if r.Err != nil {
			fmt.Printf("%-40s ERROR %v\n", r.URL, r.Err)
			continue
		}
		fmt.Printf("%-40s %d %v\n", r.URL, r.Status, r.Latency.Round(time.Millisecond))
	}
}
printf "https://go.dev\nhttps://golang.org\nhttp://localhost:9999\n" | go run .

The pipeline is: stdin → urls channel → N workers → results channel → main prints as results arrive, unbuffered so results stream out immediately rather than batching at the end. The whole batch is bounded by one shared context.WithTimeout.


Generated from real-life-golang-guide.md · 8 Oct 2026 16:36