main.go
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576
package main

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

// Task represents a unit of work to be executed concurrently
type Task struct {
	ID      int
	DelayMs int
}

// Execute runs the task with a simulated delay and returns an error if any
func (t *Task) Execute() error {
	fmt.Printf("Starting task %d (delay: %dms)\n", t.ID, t.DelayMs)
	time.Sleep(time.Duration(t.DelayMs) * time.Millisecond)
	
	// Simulate occasional error for demonstration
	if t.ID%3 == 0 {
		return fmt.Errorf("task %d failed: simulated error", t.ID)
	}
	
	fmt.Printf("Completed task %d\n", t.ID)
	return nil
}

func main() {
	// Create a list of tasks
	tasks := []Task{
		{ID: 1, DelayMs: 100},
		{ID: 2, DelayMs: 200},
		{ID: 3, DelayMs: 150},
		{ID: 4, DelayMs: 50},
	}

	// WaitGroup to wait for all goroutines to finish
	var wg sync.WaitGroup
	// Channel to collect errors from goroutines
	errChan := make(chan error, len(tasks))

	// Launch a goroutine for each task
	for _, task := range tasks {
		wg.Add(1)
		// Use loop variable copy to avoid race condition
		t := task
		go func() {
			defer wg.Done()
			if err := t.Execute(); err != nil {
				errChan <- err
			}
		}()
	}

	// Wait for all goroutines to complete then close error channel
	go func() {
		wg.Wait()
		close(errChan)
	}()

	// Collect and print all errors
	var errors []error
	for err := range errChan {
		errors = append(errors, err)
	}

	if len(errors) > 0 {
		fmt.Printf("\nEncountered %d errors:\n", len(errors))
		for _, err := range errors {
			fmt.Printf("- %v\n", err)
		}
	} else {
		fmt.Println("\nAll tasks completed successfully!")
	}
}

How It Works

Runs several independent tasks concurrently and waits for them while collecting any failures into a single report.

Builds a slice of Task structs, spins each one in its own goroutine with a WaitGroup, pushes errors into a buffered channel, then closes the channel after WaitGroup.Wait() to aggregate and print results.

Key Concepts

  • 1Copies loop variables before launching goroutines to avoid races.
  • 2Buffers the error channel to the task count so sends never block.
  • 3Uses WaitGroup to coordinate goroutine completion before closing channels.
  • 4Collates errors after the channel is closed and reports them once.

When to Use This Pattern

  • Fan-out workloads like API calls or file processing.
  • Speeding up independent jobs inside CLI tools or scripts.
  • Teaching safe goroutine patterns with error collection.
  • Prototype for bounded worker pools or pipeline stages.

Best Practices

  • Prefer contexts when tasks should be cancelable.
  • Right-size buffered channels to avoid goroutine leaks.
  • Limit concurrency if the task list can grow without bound.
  • Handle panics inside goroutines so the WaitGroup always completes.
Go Version1.13+
Difficultyintermediate
Production ReadyYes
Lines of Code76