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.