SOLID principles in Go: theory and practice
SOLID is an acronym for five fundamental principles of object-oriented design that help create flexible, maintainable, and scalable code. Although Go is not a classic OOP language (it has no class inheritance), its interfaces, composition, and strong typing allow SOLID to be applied successfully. In this article, we will break down each principle using real-world examples in Go.
1. Single Responsibility Principle (SRP)
Theory: Each type (struct, function, interface) should have only one reason to change. In other words, it is responsible for only one part of the program's functionality.
Practice in Go: A common mistake is creating "god" structs that handle business logic, logging, and sending notifications all at once.
Bad example (SRP violation):
type OrderService struct{}
func (s *OrderService) Create(order Order) error { // 1. Save to DB // 2. Send email // 3. Logging return nil}Good example (SRP compliance):
type OrderRepository interface { Save(order Order) error}
type EmailService interface { Send(to string, subject string, body string) error}
type Logger interface { Info(msg string)}
type OrderService struct { repo OrderRepository email EmailService log Logger}
func (s *OrderService) Create(order Order) error { if err := s.repo.Save(order); err != nil { s.log.Info("Failed to save order") return err } if err := s.email.Send(order.Email, "Order Confirmed", "Your order is placed"); err != nil { s.log.Info("Failed to send email") } s.log.Info("Order created successfully") return nil}Now each struct is responsible only for its own task, and changing the email logic will not affect order saving.
2. Open/Closed Principle (OCP)
Theory: Classes (types) should be open for extension but closed for modification. That is, you should be able to add new functionality without changing existing code.
Practice in Go: Use interfaces and composition to add new implementations without modifying the core logic.
Example: Suppose we have a shipping cost calculation system. Initially, there is only "standard shipping", but we need to add "express shipping".
// Define the interfacetype ShippingCostCalculator interface { Calculate(order Order) float64}
// Standard implementationtype StandardShipping struct{}
func (s StandardShipping) Calculate(order Order) float64 { return 5.99}
// New implementation without changing the code abovetype ExpressShipping struct{}
func (e ExpressShipping) Calculate(order Order) float64 { return 15.99}
// Usagefunc ProcessOrder(order Order, calculator ShippingCostCalculator) { cost := calculator.Calculate(order) fmt.Printf("Shipping cost: %.2f", cost)}We don't change ProcessOrder — we simply pass a new ExpressShipping instance. This is OCP.
3. Liskov Substitution Principle (LSP)
Theory: Objects in a program should be replaceable with instances of their subtypes without altering the correctness of the program. In Go, this means that if a function expects an interface, any implementation of that interface should work correctly.
Practice in Go: Ensure that your interface implementations do not violate the expectations of the calling code. For example, if an interface implies that the GetName() method always returns