Advanced Design Patterns in Go with Code Examples

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

Advanced Design Patterns in Go: From Theory to Practice



Go (Golang) is a language that encourages simplicity and code readability. However, as a project grows, even the most concise code can turn into chaos without proper architecture. At an advanced level, Go developers actively use design patterns adapted to the language's features: goroutines, channels, interfaces, and generics (since Go 1.18+).



In this article, we will break down 4 powerful patterns that will help you write scalable, testable, and performant code in Go. Each pattern is accompanied by a practical example.



1. Pipeline Pattern for Stream Processing



The Pipeline is one of the most popular patterns in Go for sequential data processing. It fits perfectly with the goroutine and channel model. Each processing stage runs in a separate goroutine, and data is passed through channels.



When to use: Log processing, data transformation, ETL processes, streaming video.



Example: A pipeline for filtering and transforming numbers.



package main

import "fmt"

// Stage 1: Generate numbersfunc generate(nums ...int) <-chan int { out := make(chan int) go func() { for _, n := range nums { out <- n } close(out) }() return out}

// Stage 2: Multiply by 2func multiply(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { out <- n * 2 } close(out) }() return out}

// Stage 3: Filter (only even numbers)func filterEven(in <-chan int) <-chan int { out := make(chan int) go func() { for n := range in { if n%2 == 0 { out <- n } } close(out) }() return out}

func main() { // Assemble the pipeline pipeline := filterEven(multiply(generate(1, 2, 3, 4, 5)))

for result := range pipeline { fmt.Println(result) // Output: 4, 8 (only even numbers after multiplication) }}


Pros: Natural parallel processing, low memory consumption (data is not accumulated), easy to add new stages.



2. Fan-Out / Fan-In: Scaling Processing



This pattern is an advanced evolution of the Pipeline. Fan-Out (branching) launches multiple goroutines to process data from a single channel in parallel. Fan-In (merging) collects results from multiple channels into one.



When to use: Web page parsing, processing large numbers of files, parallel API requests.



Example: Parallel URL availability check.



package main

import ( "fmt" "net/http" "sync")

func worker(urls <-chan string, results chan<- string, wg *sync.WaitGroup) { defer wg.Done() for url := range urls { resp, err := http.Get(url) if err != nil { results <- fmt.Sprintf("Error: %s - %v", url, err) continue } resp.Body.Close() results <- fmt.Sprintf("OK: %s - %d", url, resp.StatusCode) }}

func fanOutFanIn(urls []string, numWorkers int) []string { urlChan := make(chan string, len(urls)) resultChan := make(chan string, len(urls)) var wg sync.WaitGroup

// Fan-Out: launch workers for i := 0; i < numWorkers; i++ { wg.Add(1) go worker(urlChan, resultChan, &wg) }

// Send URLs for _, url := range urls { urlChan <- url } close(urlChan)

// Wait for all workers to finish wg.Wait() close(resultChan)

// Fan-In: collect results var results []string for res := range resultChan { results = append(

Blogs

Book Recommendations