Design Patterns in Go: Advanced Level for Professional Developers
\n\nGo (Golang) is a language designed for efficient work with concurrency and microservice architecture. Basic design patterns such as Singleton or Factory are well known, but creating high-load and fault-tolerant systems requires an advanced level of pattern mastery. In this article, we will analyze four key patterns: Fan-Out/Fan-In, Pipeline, Worker Pool, and Circuit Breaker. Each of them solves practical problems of scaling, parallel processing, and protecting the system from cascading failures.
\n\n1. Fan-Out / Fan-In Pattern: Parallel Data Processing
\n\nThis pattern is the foundation of parallel computing in Go. Fan-Out distributes the load among multiple goroutines, while Fan-In collects the results back into a single channel.
\n\nFan-Out Implementation
\nImagine a task: you need to process 100 URLs. Instead of sequential processing, we launch 10 goroutines, each taking a task from a common channel.
\n\npackage main\n\nimport (\n "fmt"\n "sync"\n)\n\nfunc worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {\n defer wg.Done()\n for job := range jobs {\n // Simulating processing\n results <- job * 2\n fmt.Printf("Worker %d processed job %d\\n", id, job)\n }\n}\n\nfunc main() {\n const numJobs = 20\n const numWorkers = 5\n\n jobs := make(chan int, numJobs)\n results := make(chan int, numJobs)\n var wg sync.WaitGroup\n\n // Fan-Out: launching workers\n for w := 1; w <= numWorkers; w++ {\n wg.Add(1)\n go worker(w, jobs, results, &wg)\n }\n\n // Sending tasks\n for j := 1; j <= numJobs; j++ {\n jobs <- j\n }\n close(jobs)\n\n // Fan-In: waiting for completion and closing results\n go func() {\n wg.Wait()\n close(results)\n }()\n\n // Collecting results\n for result := range results {\n fmt.Println("Result:", result)\n }\n}\n\n\nKey points:
\n- \n
- The
jobschannel serves as the Fan-Out point: all workers read from it. \n - The
resultschannel is the Fan-In point: all workers write to it. \n - Using
sync.WaitGroupensures we close results only after all goroutines have completed. \n
2. Pipeline Pattern: Conveyor Data Processing
\n\nPipeline is a chain of stages, where each stage processes data and passes it to the next. In Go, this is implemented through a sequence of channels. The pattern is ideal for ETL processes, log processing, or data transformation.
\n\nExample: Text Processing Pipeline
\n\npackage main\n\nimport "fmt"\n\n// Stage 1: number generation\nfunc generate(nums ...int) <-chan int {\n out := make(chan int)\n go func() {\n for _, n := range nums {\n out <- n\n }\n close(out)\n }()\n return out\n}\n\n// Stage 2: squaring\nfunc square(in <-chan int) <-chan int {\n out := make(chan int)\n go func() {\n for n := range in {\n out <- n * n\n }\n close(out)\n }()\n return out\n}\n\n// Stage 3: multiplication by 2\nfunc multiply(in <-chan int, factor int) <-chan int {\n out := make(chan int)\n go func() {\n for n := range in {\n out <- n * factor\n }\n close(out)\n }()\n return out\n}\n\nfunc main() {\n // Building the pipeline\n pipeline := multiply(square(generate(1, 2, 3, 4)), 10)\n\n // Consuming results\n for result := range pipeline {\n fmt.Println(result) // 10, 40, 90, 160\n }\n}\n\n\nAdvantages:
\n- \n
- Each stage is a separate