The Problem: Goroutines Don't Wait

Goroutines are flat from the scheduler's perspective—no parent-child hierarchy. A spawned goroutine is just another G in the queue.

func fetchAll() {
    go fetch("binance")   // spawns and returns immediately
    go fetch("coinbase")  // spawns and returns immediately
}  // function exits, but goroutines may still be running!

If main() exits, all goroutines are killed—even if they're still working.


WaitGroup Basics

func fetchAll() {
    var wg sync.WaitGroup
    
    wg.Add(2)
    go func() {
        defer wg.Done()
        fetch("binance")
    }()
    go func() {
        defer wg.Done()
        fetch("coinbase")
    }()
    
    wg.Wait()  // blocks until both complete
}
Method What It Does
wg.Add(n) Increment counter by n
wg.Done() Decrement counter by 1
wg.Wait() Block until counter reaches 0

Always Use defer wg.Done()

// ✓ Correct
go func() {
    defer wg.Done()  // always runs, even on panic
    fetch("binance")
}()

// ✗ Wrong
go func() {
    fetch("binance")
    wg.Done()  // skipped if fetch() panics → wg.Wait() blocks forever
}()
Pattern On Success On Panic
defer wg.Done() ✓ Runs ✓ Runs
wg.Done() at end ✓ Runs ✗ Skipped

Always defer wg.Done() at the start of the goroutine, not wg.Done() at the end.


Nested WaitGroups (Goroutines of Goroutines)

When you have goroutines spawning goroutines: each level has its own WaitGroup.

Example: 5 Exchanges × 2-5 Endpoints Each

func main() {
    var exchangeWg sync.WaitGroup
    exchanges := []string{"binance", "coinbase", "kraken", "okx", "bybit"}
    
    for _, exchange := range exchanges {
        exchangeWg.Add(1)
        go func(ex string) {
            defer exchangeWg.Done()
            
            // Each exchange has its OWN WaitGroup for endpoints
            var endpointWg sync.WaitGroup
            endpoints := getEndpoints(ex)  // returns 2-5 endpoints
            
            for _, endpoint := range endpoints {
                endpointWg.Add(1)
                go func(ep string) {
                    defer endpointWg.Done()
                    fetch(ex, ep, bus)
                }(endpoint)
            }
            
            endpointWg.Wait()  // exchange waits for ALL its endpoints
            log.Printf("%s: all endpoints done", ex)
            
        }(exchange)
    }
    
    exchangeWg.Wait()  // main waits for ALL exchanges
    log.Println("all exchanges done")
}

Visualized

main()
  │
  ├─ exchangeWg.Wait() ─────────────────────────────────────┐
  │                                                         │
  ├─► G: binance ──┬─ endpointWg.Wait() ──┐                │
  │                ├─► G: spot            │                │
  │                ├─► G: futures         │                │
  │                └─► G: options ────────┘─► Done() ──────┤
  │                                                         │
  ├─► G: coinbase ─┬─ endpointWg.Wait() ──┐                │
  │                ├─► G: spot            │                │
  │                └─► G: futures ────────┘─► Done() ──────┤
  │                                                         │
  └─► G: kraken... ─────────────────────────► Done() ──────┘