Frontend Labs
⋯

Day 12 — Goroutines (Introduction)

Learning Objectives

  • Understand goroutines as lightweight user-space threads (not OS threads)
  • Detect goroutine leaks — one of the most common production bugs
  • Know when to use goroutines vs sequential code

Key Concepts

// Starting a goroutine — the "go" keyword
go func() {
    // runs concurrently
}()

// Goroutines are cheap: ~2KB stack, grows as needed
// OS threads: ~1MB stack, fixed
// Rule of thumb: 10,000 goroutines is normal; 1,000,000 is unusual but possible

// Goroutine leak — goroutine blocked forever with no way to exit
func leak() {
    ch := make(chan int)
    go func() {
        val := <-ch  // blocks forever — no one sends
        fmt.Println(val)
    }()
    // function returns, goroutine still running — LEAK
}

// Fix: use context for cancellation
func noLeak(ctx context.Context) {
    ch := make(chan int)
    go func() {
        select {
        case val := <-ch:
            fmt.Println(val)
        case <-ctx.Done():
            return // goroutine exits when context is cancelled
        }
    }()
}

Exercise: Goroutine Lifecycle Observer

package main

import (
    "fmt"
    "math/rand"
    "runtime"
    "sync"
    "time"
)

func main() {
    var wg sync.WaitGroup
    start := runtime.NumGoroutine()
    fmt.Printf("goroutines before: %d\n", start)

    // Start 1000 goroutines
    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            // Random sleep 1-100ms
            time.Sleep(time.Duration(rand.Intn(100)+1) * time.Millisecond)
        }(i)
    }

    // Monitor goroutine count every 50ms
    done := make(chan struct{})
    go func() {
        for {
            select {
            case <-done:
                return
            case <-time.After(50 * time.Millisecond):
                fmt.Printf("  active goroutines: %d\n", runtime.NumGoroutine())
            }
        }
    }()

    wg.Wait()
    close(done)

    // Give monitor goroutine time to exit
    time.Sleep(100 * time.Millisecond)
    end := runtime.NumGoroutine()
    fmt.Printf("goroutines after: %d (started with %d)\n", end, start)
    if end > start+5 {
        fmt.Printf("WARNING: possible goroutine leak! leaked %d goroutines\n", end-start)
    }
}

Real-World Context

Every net.Conn handler in your TCP proxy spawns goroutines. Under a DDoS, you might get 100,000 simultaneous connections — that's 200,000 goroutines (read + write per conn). Goroutine leaks compound: a leak of 1 goroutine per request at 1000 rps = 1000 leaked goroutines/second = OOM in minutes.


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