Concurrency is Go’s superpower. Unlike OS threads which are heavy (1MB+ stack), Go uses Goroutines (2KB stack). A single Go program can easily run tens of thousands of concurrent tasks.This chapter explores goroutines, channels, and demonstrates the Worker Pool pattern — a production-ready approach to managing concurrent tasks.
func say(s string) { for i := 0; i < 5; i++ { time.Sleep(100*time.Millisecond) fmt.Println(s) }}func main() { go say("world") // Runs concurrently say("hello") // Runs in main goroutine}
The go keyword launches a new goroutine. The function executes concurrently with the rest of the program.
func printNumbers() { for i := 0; i < 5; i++ { time.Sleep(100*time.Millisecond) fmt.Println(i) }}func printLetters(){ for ch:='a';ch<='e';ch++{ time.Sleep(100*time.Millisecond) fmt.Println(string(ch)) }}func main() { go printNumbers() go printLetters() time.Sleep(1* time.Second) // Wait for goroutines to finish}
Problem: Using time.Sleep() to wait for goroutines is unreliable. What if they take longer than expected? This is where channels and WaitGroups come in.
Instead of spawning a new goroutine for every single job (which can crash a system under load), we start a fixed number of workers that pick jobs from a queue.
func worker(wg *sync.WaitGroup, resultChan chan string, jobsChan chan string) { defer wg.Done() // Ensure Done is called when function exits for url := range jobsChan { // Simulate network delay time.Sleep(time.Millisecond * 50) fmt.Println("Fetching URL:", url) resultChan <- "Fetched " + url }}
3
The for url := range jobsChan loop automatically stops when the channel is closed.
4
Step 2: Set Up Channels and WaitGroup
5
jobs := []string{ "http://example.com/image1.jpg", "http://example.com/image2.jpg", "http://example.com/image3.jpg", "http://example.com/image4.jpg", "http://example.com/image5.jpg",}var wg sync.WaitGrouptotalWorkers := 5resultChan := make(chan string, len(jobs))jobsChan := make(chan string, len(jobs))
6
Step 3: Start Workers
7
for i := 0; i < totalWorkers; i++ { wg.Add(1) go worker(&wg, resultChan, jobsChan)}
8
Step 4: Send Jobs and Close Job Channel
9
for _, job := range jobs { jobsChan <- job}close(jobsChan) // Signal: no more jobs coming
10
Step 5: Wait and Close Result Channel
11
go func() { wg.Wait() // Wait for all workers to finish close(resultChan) // Close result channel}()
12
Critical: We wait in a separate goroutine to avoid deadlock. If we waited in the main thread before reading results, the program could deadlock if the result channel fills up.
13
Step 6: Collect Results
14
for result := range resultChan { fmt.Println("Result received:", result)}fmt.Println("Total time taken:", time.Since(start))
var wg sync.WaitGroupwg.Add(1) // Increment countergo func() { defer wg.Done() // Decrement when done // ... work ...}()wg.Wait() // Block until counter reaches 0