Frontend Labs
⋯

Day 13 — Channels

Learning Objectives

  • Use channels as typed message queues between goroutines
  • Know when buffered vs unbuffered channels are appropriate
  • Build a worker pool — the pattern used in concurrent HTTP checkers, DNS resolvers, and log processors

Key Concepts

// Unbuffered channel — send blocks until receiver is ready (synchronizes goroutines)
ch := make(chan int)

// Buffered channel — send only blocks when full (decouples producer/consumer)
ch := make(chan int, 100)

// Closing a channel signals no more values
close(ch)
for v := range ch { } // reads until closed and drained

// select — non-blocking multi-channel dispatch
select {
case msg := <-inbound:
    handle(msg)
case <-ctx.Done():
    return ctx.Err()
case <-time.After(5 * time.Second):
    return errors.New("timeout")
default:
    // non-blocking: runs if no channel is ready
}

Exercise: Worker Pool for URL Checking

package main

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

type Job struct {
    URL string
}

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

func worker(ctx context.Context, id int, jobs <-chan Job, results chan<- Result, wg *sync.WaitGroup) {
    defer wg.Done()
    client := &http.Client{Timeout: 5 * time.Second}

    for {
        select {
        case job, ok := <-jobs:
            if !ok {
                return // channel closed, worker exits
            }
            start := time.Now()
            resp, err := client.Get(job.URL)
            result := Result{URL: job.URL, Latency: time.Since(start), Err: err}
            if err == nil {
                result.Status = resp.StatusCode
                resp.Body.Close()
            }
            select {
            case results <- result:
            case <-ctx.Done():
                return
            }
        case <-ctx.Done():
            return
        }
    }
}

func main() {
    urls := []string{
        "https://example.com",
        "https://golang.org",
        "https://httpbin.org/status/200",
        "https://httpbin.org/status/404",
        "https://httpbin.org/delay/6", // will timeout
    }

    ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    jobs := make(chan Job, len(urls))
    results := make(chan Result, len(urls))

    // Start 3 workers
    var wg sync.WaitGroup
    for i := 0; i < 3; i++ {
        wg.Add(1)
        go worker(ctx, i, jobs, results, &wg)
    }

    // Send all jobs, close channel when done
    for _, url := range urls {
        jobs <- Job{URL: url}
    }
    close(jobs)

    // Wait for workers, then close results
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collect results
    for r := range results {
        if r.Err != nil {
            fmt.Printf("ERROR  %s: %v\n", r.URL, r.Err)
        } else {
            fmt.Printf("%d    %s (%v)\n", r.Status, r.URL, r.Latency.Round(time.Millisecond))
        }
    }
}

Real-World Context

This worker pool pattern appears everywhere: health checkers (ping 50 backends every 5s), log processors (fan-out to multiple sinks), WAF rule testers (validate 1000 payloads against rules). The ctx.Done() case in select ensures workers exit cleanly when the parent context times out.


Generated from week2.md · 8 Oct 2026 16:36