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.