+1

🐹 Common Design Patterns In Golang Projects 🧩

Golang is widely used for building scalable and performant systems. Due to its simplicity and strong support for concurrency, some design patterns are more common in Golang programs compared to other languages. Here are the most commonly used design patterns in Go:

1. Traditional Design Patterns 📐

1. Singleton Pattern

Ensures a single instance of a type exists across the application. It is often used for resources like configuration files, logging, or database connections.

Example:

package singleton

import "sync"

type Singleton struct{}

var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
    })
    return instance
}

Usage: Loggers, database connections, or shared configurations.

2. Factory Pattern

Provides a method to create objects without exposing the creation logic. It abstracts the instantiation process.

Example:

package factory

import "fmt"

type Animal interface {
    Speak() string
}

type Dog struct{}
func (d Dog) Speak() string { return "Woof!" }

type Cat struct{}
func (c Cat) Speak() string { return "Meow!" }

func AnimalFactory(animalType string) Animal {
    switch animalType {
    case "dog":
        return Dog{}
    case "cat":
        return Cat{}
    default:
        return nil
    }
}

func main() {
    animal := AnimalFactory("dog")
    fmt.Println(animal.Speak())
}

Usage: Object creation for different types dynamically.

3. Decorator Pattern

Dynamically adds behaviors to objects at runtime without modifying their code. In Go, this is achieved through functions.

Example:

package main

import "fmt"

type Notifier interface {
    Send(message string)
}

type EmailNotifier struct{}

func (e EmailNotifier) Send(message string) {
    fmt.Println("Email: " + message)
}

func WithSMSNotifier(notifier Notifier) Notifier {
    return &struct{ Notifier }{
        Notifier: notifier,
    }
}

func main() {
    email := EmailNotifier{}
    email.Send("Hello")

    smsNotifier := WithSMSNotifier(email)
    smsNotifier.Send("Hello with SMS")
}

Usage: Adding logging, caching, or metrics around existing components.

4. Observer Pattern

Defines a one-to-many dependency between objects so that when one object changes state, all dependents are notified.

Example:

package observer

import "fmt"

type Observer interface {
    Update(string)
}

type Subject struct {
    observers []Observer
}

func (s *Subject) Register(o Observer) {
    s.observers = append(s.observers, o)
}

func (s *Subject) Notify(data string) {
    for _, observer := range s.observers {
        observer.Update(data)
    }
}

type EmailClient struct{}

func (e EmailClient) Update(data string) {
    fmt.Println("Email received:", data)
}

func main() {
    subject := Subject{}
    emailClient := EmailClient{}

    subject.Register(emailClient)
    subject.Notify("New Update Available!")
}

Usage: Event-driven systems or pub-sub implementations.

5. Strategy Pattern

Defines a family of algorithms, encapsulates each one, and makes them interchangeable.

Example:

package strategy

import "fmt"

type Strategy interface {
    Execute(a, b int) int
}

type Add struct{}
func (Add) Execute(a, b int) int { return a + b }

type Multiply struct{}
func (Multiply) Execute(a, b int) int { return a * b }

func main() {
    var strategy Strategy = Add{}
    fmt.Println("Add:", strategy.Execute(2, 3))

    strategy = Multiply{}
    fmt.Println("Multiply:", strategy.Execute(2, 3))
}

Usage: Selecting algorithms at runtime, e.g., sorting strategies.

6. Adapter Pattern

Allows incompatible interfaces to work together by providing a bridge.

Example:

package main

import "fmt"

type OldPrinter interface {
    PrintOldMessage() string
}

type LegacyPrinter struct{}

func (lp *LegacyPrinter) PrintOldMessage() string {
    return "Legacy Printer: Old message"
}

type NewPrinterAdapter struct {
    oldPrinter *LegacyPrinter
}

func (npa *NewPrinterAdapter) PrintMessage() string {
    return npa.oldPrinter.PrintOldMessage() + " - adapted"
}

func main() {
    adapter := NewPrinterAdapter{&LegacyPrinter{}}
    fmt.Println(adapter.PrintMessage())
}

Usage: Integrating legacy code with new systems.

7. Builder Pattern

Simplifies the construction of complex objects step by step.

Example:

package main

import "fmt"

type Car struct {
    Wheels int
    Color  string
}

type CarBuilder struct {
    car Car
}

func (cb *CarBuilder) SetWheels(wheels int) *CarBuilder {
    cb.car.Wheels = wheels
    return cb
}

func (cb *CarBuilder) SetColor(color string) *CarBuilder {
    cb.car.Color = color
    return cb
}

func (cb *CarBuilder) Build() Car {
    return cb.car
}

func main() {
    car := CarBuilder{}.
        SetWheels(4).
        SetColor("Red").
        Build()
    fmt.Println(car)
}

Usage: Constructing complex structs like configurations.

8. Chain of Responsibility Pattern

Passes requests along a chain of handlers.

Example:

package main

import "fmt"

type Handler interface {
    SetNext(handler Handler)
    Handle(request string)
}

type BaseHandler struct {
    next Handler
}

func (b *BaseHandler) SetNext(handler Handler) {
    b.next = handler
}

func (b *BaseHandler) Handle(request string) {
    if b.next != nil {
        b.next.Handle(request)
    }
}

type ConcreteHandler struct {
    BaseHandler
    name string
}

func (ch *ConcreteHandler) Handle(request string) {
    fmt.Println(ch.name, "handling request:", request)
    ch.BaseHandler.Handle(request)
}

func main() {
    handler1 := &ConcreteHandler{name: "Handler 1"}
    handler2 := &ConcreteHandler{name: "Handler 2"}

    handler1.SetNext(handler2)
    handler1.Handle("Process this")
}

Usage: Middleware in HTTP servers.

9. Command Pattern

Encapsulates a request as an object.

Example:

package main

import "fmt"

type Command interface {
    Execute()
}

type Light struct{}

func (l Light) On() {
    fmt.Println("Light is On")
}

type LightOnCommand struct {
    light Light
}

func (c LightOnCommand) Execute() {
    c.light.On()
}

func main() {
    light := Light{}
    command := LightOnCommand{light: light}

    command.Execute()
}

Usage: Task queues or undo operations.

10. Options Pattern

The Options Pattern provides a way to create flexible, configurable objects by using functional options instead of constructors with many parameters.

Example:

package main

import "fmt"

// Product represents the product configuration
type Product struct {
    Name  string
    Price float64
}

// Option is a function that modifies the Product configuration
type Option func(*Product)

// NewProduct creates a new Product with optional configurations
func NewProduct(options ...Option) *Product {
    p := &Product{} // default product
    for _, option := range options {
        option(p)
    }
    return p
}

// WithName sets the product's name
func WithName(name string) Option {
    return func(p *Product) {
        p.Name = name
    }
}

// WithPrice sets the product's price
func WithPrice(price float64) Option {
    return func(p *Product) {
        p.Price = price
    }
}

func main() {
    product := NewProduct(WithName("Laptop"), WithPrice(1200.50))
    fmt.Println("Product:", *product)
}

Usage: This pattern is commonly used when you need to provide optional configuration for an object, allowing users to choose which options to set.

11. Error Wrapper Pattern

The Error Wrapper Pattern is used to enhance errors by adding context (e.g., additional details or stack traces) to make debugging easier.

Example:

package main

import (
    "fmt"
    "errors"
)

// ErrorWrapper wraps an existing error with additional context
type ErrorWrapper struct {
    msg   string
    inner error
}

// Error implements the error interface
func (e *ErrorWrapper) Error() string {
    return fmt.Sprintf("%s: %v", e.msg, e.inner)
}

// WrapError creates a new wrapped error
func WrapError(msg string, err error) *ErrorWrapper {
    return &ErrorWrapper{
        msg:   msg,
        inner: err,
    }
}

func main() {
    err := errors.New("database connection failed")
    wrappedErr := WrapError("unable to connect to database", err)
    fmt.Println(wrappedErr)
}

Usage: This pattern is useful for adding context to errors, such as including the location or description of the error. It’s particularly helpful in complex systems where multiple layers of abstraction exist.

Summary:

Pattern Usage
Singleton Shared resources (e.g., config, DB)
Factory Object creation logic
Decorator Adding functionality dynamically
Observer Event-driven systems
Strategy Selecting algorithms dynamically
Adapter Bridging incompatible interfaces
Builder Building complex objects
Chain of Responsibility Middleware or request handlers
Command Queues, undo-redo functionality
Options Flexible object creation with functional options
Error Wrapper Enhancing errors with context or stack trace

2. Concurrency Patterns 🔄

In Golang, besides traditional design patterns, developers frequently utilize concurrency patterns and other idiomatic patterns specific to Go's strengths. These patterns focus on leveraging Go’s concurrency primitives, such as goroutines, channels, and select statements, as well as structuring code for readability and maintainability.

1. Worker Pool Pattern

Used to limit the number of concurrent tasks being executed, improving resource utilization and system stability.

Example:

package main

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

func worker(id int, jobs <-chan int, results chan<- int) {
	for j := range jobs {
		fmt.Printf("Worker %d processing job %d\n", id, j)
		time.Sleep(time.Second) // Simulate work
		results <- j * 2        // Return result
	}
}

func main() {
	const numWorkers = 3
	const numJobs = 5

	jobs := make(chan int, numJobs)
	results := make(chan int, numJobs)

	// Start worker goroutines
	for w := 1; w <= numWorkers; w++ {
		go worker(w, jobs, results)
	}

	// Send jobs to the channel
	for j := 1; j <= numJobs; j++ {
		jobs <- j
	}
	close(jobs)

	// Collect results
	for a := 1; a <= numJobs; a++ {
		fmt.Printf("Result: %d\n", <-results)
	}
}
  • Use case: Tasks like processing HTTP requests, file uploads, or batch jobs.
  • Benefit: Controls the number of concurrent workers, prevents system overload.

2. Fan-Out, Fan-In Pattern

  • Fan-Out: Distribute tasks to multiple goroutines to process concurrently.
  • Fan-In: Combine results from multiple goroutines into a single channel.

Example:

package main

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

func producer(ch chan int) {
	for i := 1; i <= 5; i++ {
		ch <- i
		time.Sleep(time.Millisecond * 100)
	}
	close(ch)
}

func worker(id int, ch <-chan int, results chan<- int, wg *sync.WaitGroup) {
	defer wg.Done()
	for job := range ch {
		fmt.Printf("Worker %d processing %d\n", id, job)
		results <- job * 2
	}
}

func main() {
	jobs := make(chan int, 5)
	results := make(chan int, 5)

	// Fan-Out: Start workers
	var wg sync.WaitGroup
	for w := 1; w <= 3; w++ {
		wg.Add(1)
		go worker(w, jobs, results, &wg)
	}

	// Fan-In: Collect results
	go producer(jobs)
	go func() {
		wg.Wait()
		close(results)
	}()

	for res := range results {
		fmt.Println("Result:", res)
	}
}
  • Use case: Processing large volumes of tasks in parallel, e.g., web scraping.
  • Benefit: Efficiently utilizes multiple goroutines and aggregates results.

3. Rate Limiting Pattern

Controls the rate of operations to prevent overloading downstream systems.

Example:

package main

import (
	"fmt"
	"time"
)

func main() {
	rateLimit := time.Tick(500 * time.Millisecond) // Allow 1 task every 500ms

	for i := 1; i <= 5; i++ {
		<-rateLimit
		fmt.Println("Processing task", i, "at", time.Now())
	}
}
  • Use case: API rate limiting, preventing resource overuse.
  • Benefit: Ensures tasks are processed at a steady, controlled rate.

4. Pipeline Pattern

Passes data through a series of processing stages using channels.

Example:

package main

import "fmt"

func generator(nums ...int) <-chan int {
	out := make(chan int)
	go func() {
		for _, n := range nums {
			out <- n
		}
		close(out)
	}()
	return out
}

func square(in <-chan int) <-chan int {
	out := make(chan int)
	go func() {
		for n := range in {
			out <- n * n
		}
		close(out)
	}()
	return out
}

func main() {
	nums := generator(1, 2, 3, 4)
	squares := square(nums)

	for n := range squares {
		fmt.Println(n)
	}
}
  • Use case: Data transformations in steps (e.g., ETL pipelines).
  • Benefit: Clear separation of stages, scalable for large data processing.

3. Other Design Patterns 🛠️

5. Repository Pattern

Abstracts the database layer, ensuring separation of concerns and clean code.

Example:

package repository

type User struct {
	ID   int
	Name string
}

type UserRepository interface {
	GetByID(id int) (*User, error)
}

type userRepo struct{}

func (u userRepo) GetByID(id int) (*User, error) {
	// DB logic here (e.g., SELECT query)
	return &User{ID: id, Name: "Alice"}, nil
}

func NewUserRepository() UserRepository {
	return userRepo{}
}
  • Use case: Microservices or complex business logic needing abstraction.
  • Benefit: Easier to test and change the database layer.

6. Pub/Sub Pattern

Implements an event-driven communication model between components.

Example: Using channels for event propagation:

package main

import "fmt"

func publisher(ch chan<- string) {
	ch <- "Event 1"
	ch <- "Event 2"
	close(ch)
}

func subscriber(ch <-chan string) {
	for event := range ch {
		fmt.Println("Received:", event)
	}
}

func main() {
	events := make(chan string)
	go publisher(events)
	subscriber(events)
}
  • Use case: Event-driven systems, message broadcasting.
  • Benefit: Decouples event producers and consumers.

7. Configuration Pattern

Centralizes configuration management for maintainability and consistency.

Example:

package config

import (
	"fmt"
	"os"
)

type Config struct {
	Port string
}

func LoadConfig() Config {
	return Config{
		Port: os.Getenv("APP_PORT"),
	}
}

func main() {
	os.Setenv("APP_PORT", "8080")
	config := LoadConfig()
	fmt.Println("App Port:", config.Port)
}
  • Use case: Managing environment variables or YAML/JSON configs.
  • Benefit: Promotes clean configuration management.

8. Circuit Breaker Pattern

Prevents cascading failures in distributed systems by halting failing operations.

Summary: Common Golang Patterns

Category Patterns
Concurrency Worker Pool, Fan-Out/Fan-In, Pipeline, Rate Limiting
Behavioral Observer, Strategy, Chain of Responsibility, Pub/Sub
Creational Singleton, Factory, Builder
Structural Adapter, Decorator, Repository
Other Circuit Breaker, Configuration

By combining these patterns, Golang projects achieve modularity, scalability, and efficient concurrency handling—essential for modern distributed systems.

If you found this helpful, let me know by leaving a 👍 or a comment!, or if you think this post could help someone, feel free to share it! Thank you very much! 😃


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí