Gaurd strategy

func main() {
    maxGoroutines := 10
    guard := make(chan struct{}, maxGoroutines)
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)
        guard <- struct{}{} // Blocks if channel is full (reaches cap)
        go func(n int) {
            defer wg.Done()
            defer func() { <-guard }() // Release slot when done
            doWork(n)
        }(i)
    }
    wg.Wait()
}

Collect errors if needed in another buffered channel


func main() {
    size := 100
    maxGoroutines := 10
    guard := make(chan struct{}, maxGoroutines)
    theErrors := make(chan error, size)
    var wg sync.WaitGroup

    for i := 0; i < size; i++ {
        wg.Add(1)
        guard <- struct{}{} // Blocks if channel is full (reaches cap)
        go func(n int) {
            defer wg.Done()
            defer func() { <-guard }() // Release slot when done
            err := doWork(n)
            if err != nil {
                theErrors <- fmt.Errorf("error, when doing the work. Error: %v", err)
                return
            }
        }(i)
    }

    wg.Wait()
    select {
        case e := <- theErrors:
            log.Fatalf("error, encountered during work. Error: %v", e)
        default:
    }
}