Lab 07: Goroutines & Channels

Objective

Write concurrent Go programs using goroutines and channels: fan-out/fan-in, worker pools, select statements, and channel directions.

Time

35 minutes

Prerequisites

  • Lab 01–06

Tools

  • Docker image: zchencow/innozverse-go:latest


Lab Instructions

Step 1: Goroutines

docker run --rm zchencow/innozverse-go:latest go run - << 'EOF'
package main

import (
    "fmt"
    "sync"
    "time"
)

func sayHello(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Goroutine %d: Hello!\n", id)
}

func countDown(from int) {
    for i := from; i >= 0; i-- {
        fmt.Printf("T-%d\n", i)
        time.Sleep(10 * time.Millisecond)
    }
}

func main() {
    // Launch goroutines and wait for all to finish
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go sayHello(i, &wg)
    }
    wg.Wait()
    fmt.Println("All goroutines done")

    // Goroutines are lightweight — spawn thousands easily
    fmt.Println("\nCounting down...")
    var wg2 sync.WaitGroup
    wg2.Add(1)
    go func() {
        defer wg2.Done()
        countDown(3)
    }()
    wg2.Wait()
    fmt.Println("Launch!")
}
EOF

💡 Goroutines cost ~2KB of stack (vs ~1MB for OS threads) and the runtime multiplexes them across CPU cores. You can have millions of goroutines simultaneously. sync.WaitGroup is the standard way to wait for a group to finish — Add(1) before launching, Done() in the goroutine (always via defer), Wait() to block.

📸 Verified Output:


Step 2: Channels

💡 Close a channel when the sender is done — receivers can use range ch to consume all values and stop automatically when the channel closes. Only the sender should close a channel, never the receiver. Sending to a closed channel panics. Receiving from a closed channel returns the zero value immediately.

📸 Verified Output:


Step 3: Select Statement

💡 select is the heart of Go concurrency. It waits for whichever channel is ready first. time.After(d) returns a channel that receives after duration d — perfect for timeouts. The default case makes select non-blocking. select {} (empty select) blocks forever — useful as a sleep(forever).

📸 Verified Output:


Step 4: Fan-out / Fan-in

📸 Verified Output:


Steps 5–8: Worker Pool, Done Channel, Mutex vs Channel, Capstone

📸 Verified Output:


Summary

Concept
Pattern
Use case

Goroutine

go fn()

Concurrent execution

WaitGroup

wg.Add/Done/Wait

Wait for N goroutines

Channel

make(chan T)

Communication between goroutines

Select

select { case <-ch }

Wait on multiple channels

Worker pool

N goroutines reading from 1 channel

Parallel task processing

Mutex

mu.Lock/Unlock

Protect shared mutable state

Atomic

atomic.AddInt64

Simple counter without mutex

Further Reading

Last updated