- vừa được xem lúc

🐹 Common Design Patterns In Golang Projects 🧩

0 0 1

Người đăng: Truong Phung

Theo Viblo Asia

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! 😃

Bình luận

Bài viết tương tự

- vừa được xem lúc

Embedded Template in Go

Getting Start. Part of developing a web application usually revolves around working with HTML as user interface.

0 0 57

- vừa được xem lúc

Tại sao bạn nên học Go ?

. Trong những năm gần đây, có một sự trỗi dậy của một ngôn ngữ lập trình mới Go hay Golang. Không có gì làm cho các developer chúng ta phát cuồng hơn một ngôn ngữ mới, phải không ?.

0 0 35

- vừa được xem lúc

Implement Multi-Connections Download in Go - Part II

Getting Start. In Part I we've looked at how to implement core download functionality as well as define a public API for client to use our package.

0 0 25

- vừa được xem lúc

Sự khó chịu khi mới học Golang và cách giải quyết siêu đơn giản

Gần đây mình đang tìm một giải pháp backend thay thế cho Java Spring (do nó nặng vãi chưởng), sẵn tiện học luôn ngôn ngữ mới. Bỏ ra một ngày để tìm hiểu, khảo sát vài ngôn ngữ backend, cuối cùng mình

0 0 58

- vừa được xem lúc

Serverless Series (Golang) - Bài 9 - Codepipeline Notification with AWS Chatbot and AWS SNS

Giới thiệu. Chào các bạn tới với series về Serverless, ở bài trước chúng ta đã tìm hiểu về cách dựng CI/CD với Codepipeline.

0 0 24

- vừa được xem lúc

Golang và Goroutines

Giới thiệu. Dạo gần đây mình có tìm hiểu về Go hay còn được gọi là Golang thì mình thấy Go có đặc điểm nổi bật là có tốc độ xử lý nhanh và hỗ trợ xử lý đa luồng (concurrency) rất tốt với Goroutines.

0 0 48